NodeJS模块原理分析
1.nodejs模块简介
- 在CommonJS规范中一个文件就是一个模块。
- 在CommonJS规范中通过exports暴露数据。
- 在CommonJS规范中通过require()导入模块。
2.怎么执行读取的文件
我们都知道通过fs模块可以读取文件,但是读取到的数据要么是二进制,要么是字符串。无论是二进制还是字符串都无法直接执行。但是我们知道如果是字符串,在JS中是有办法让它执行的:eval或者new Function;
2.1 通过eval执行代码
缺点:存在依赖关系,字符串可以访问外界数据,不安全。
let str = 'console.log("hello")'
eval(str) // hello
// 存在依赖关系, 字符串可以访问外界数据,不安全
let name = "lgg";
let str1 = "console.log(name);";
eval(str1); // lgg
2.2 通过new Function执行代码
缺点:存在依赖关系,依然可以访问全局数据,不安全。
let str = "console.log('aaaa');";
let fn = new Function(str);
console.log(fn);
/*打印: anonymous() {
console.log('aaaa');
}
*/
fn(); // aaaa
// 存在依赖关系, 字符串可以访问外界数据,不安全
let name = "lgg";
let str = "console.log(name);";
let fn = new Function(str);
2.3 通过NodeJS的vm虚拟机执行代码
2.3.1 runInThisContext
提供了一个安全的环境给我们执行字符串中的代码。runInThisContext提供的环境不能访问本地的变量,但是可以访问全局的变量(也就是global上的变量)。
const vm = require('vm')
let str = "console.log('lgg')"
vm.runInThisContext(str) // lgg
let name1 = 'lgg'
let str1 = "console.log(name1)"
vm.runInThisContext(str1) // name is not defined
global.name2 = 'lgg'
let str2 = "console.log(name2)"
vm.runInThisContext(str2) // lgg
2.3.2 runInNewContext
提供了一个安全的环境给我们执行字符串中的代码。提供的环境不能访问本地的变量,也不能访问全局的变量(也就是global上的变量)。
let name1 = "lgg"
let str1 = "console.log(name1)"
vm.runInNewContext(str1) // name1 is not defined
global.name2 = "lgg"
let str2 = "console.log(name2)"
vm.runInNewContext(str2) // name2 is not defined
3.Node模块原理分析
既然一个文件就是一个模块,既然想要使用模块必须先通过require()
导入模块。所以可以推断出require()
的作用其实就是读取文件。所以想要了解Node是如何实现模块的,必须先了解如何执行读取到的代码。
3.1 Node模块加载流程分析
(1)内部实现了一个require方法。
function require(path) {
return self.require(path);
}
(2)通过Module对象的静态__load方法加载模块文件。
Module.prototype.require = function(path) {
return Module._load(path, this, /* isMain */ false);
};
(3)通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名。
var filename = Module._resolveFilename(request, parent, isMain);
(4)根据路径判断是否有缓存, 如果没有就创建一个新的Module模块对象并缓存起来。
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
var module = new Module(filename, parent);
Module._cache[filename] = module;
function Module(id, parent) {
this.id = id;
this.exports = {
};
}
(5)利用tryModuleLoad方法加载模块tryModuleLoad(module, filename)。
- 取出模块后缀
var extension = path.extname(filename);
- 根据不同后缀查找不同方法并执行对应的方法, 加载模块
Module._extensions[extension](this, filename);
- 如果是JSON就转换成对象
module.exports = JSON.parse(internalModule.stripBOM(content));
- 如果是JS就包裹一个函数
var wrapper = Module.wrap(content); NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', ‘\n});’ ];
- 执行包裹函数之后的代码, 拿到执行结果(String – Function)
var compiledWrapper = vm.runInThisContext(wrapper);
- 利用call执行fn函数, 修改module.exports的值
var args = [this.exports, require, module, filename, dirname]; var result = compiledWrapper.call(this.exports, args);
- 返回module.exports
return module.exports;
3.2 自己实现一个require方法
const path = require('path')
const fs = require('fs')
const vm = require('vm')
class NJModule {
constructor(id) {
this.id = id; // 保存当前模块的绝对路径
this.exports = {
}
}
}
NJModule._cache = {
}
NJModule.wrapper = ['(function (exports, require, module, __filename, __dirname) { ', '\n});'];
NJModule._extensions = {
'.js': function(module) {
// 1、读取js代码
let script = fs.readFileSync(module.id)
// 2、将JS代码包裹到函数中
/*
(function (exports, require, module, __filename, __dirname) {
exports.名 = 值;
});
* */
let strScript = NJModule.wrapper[0] + script + NJModule.wrapper[1]
// 3、将字符串转换成JS代码
let jsScript = vm.runInThisContext(strScript)
// 4、执行转换后的JS代码
// var args = [this.exports, require, module, filename, dirname];
// var result = compiledWrapper.call(this.exports, args);
jsScript.call(module.exports, module.exports) // 在函数中使用的exports就是对象(即module.exports)的exports。因为是对象为引用关系。
},
'.json': function(module) {
let json = fs.readFileSync(module.id);
let obj = JSON.parse(json);
module.exports = obj
}
}
function tryModuleLoad(module) {
// 4.1取出模块后缀
let extName = path.extname(module.id)
NJModule._extensions[extName](module)
}
function njRequire(filePath) {
// 1.将传入的相对路径转换成绝对路径
let absPath = path.join(__dirname, filePath)
// 2.尝试从缓存中获取当前的模块
let cachedModule = NJModule._cache[absPath]
if(cachedModule) {
return cachedModule.exports;
}
// 3.如果没有缓冲就自己创建一个njModule对象,并缓存起来
var module = new NJModule(absPath)
NJModule._cache[absPath] = module
// 4.利用tryModuleLoad方法加载模块
tryModuleLoad(module);
// 5.返回模块的exports
return module.exports
}
下面来测试下:
//test.js
exports.a=12
exports.fn=()=>{
console.log("111")
}
//person.js
{
"name": "bob",
"age": 12
}
//index.js
let test=njRequire("./test.js");
console.log(test)//{ a: 12, fn: [Function (anonymous)] }
test.fn();//111
let person=njRequire("./person.json");
console.log(person)//{ name: 'bob', age: 12 }
4.一些思考
4.1 NodeJS中为什么可以直接使用exports、require、module、__filename、__dirname?
因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中,这些属性都被通过参数的形式传递过来了。
(function (exports, require, module, __filename, __dirname) {
exports.名 = 值;
});
4.2 NodeJS中为什么不能直接exports赋值,而可以给module.exports赋值?
(function (exports, require, module, __filename, __dirname) {
exports = "lnj";
});
jsScript.call(module.exports, module.exports);
return module.exports;
//相当于
let exports = module.exports;
exports = "lnj";
return module.exports;
exports是形参,module.exports传递给它,两者指向同一个对象。如果直接给exports赋值(exports=‘aaa’)则相当于修改了它的指向,但最后却返回module.exports。
let moduleA={
exportsA:{
}
}
let exportsA=moduleA.exportsA;
// exportsA.aaa=222;
// console.log("moduleA",moduleA);//moduleA { exportsA: { aaa: 222 } }
// console.log("exportsA",exportsA);//exportsA { aaa: 222 }
exportsA=222;
exportsA.aaa=333;
console.log("moduleA",moduleA);//moduleA { exportsA: {} }
console.log("exportsA",exportsA);//exportsA 222
4.3 通过require导入包的时候应该使用var/let还是const?
导入包的目的是使用包而不是修改包,所以导入包时使用const接受。
4.4 require和import的区别
import和require都是被模块化所使用。在ES6当中,用export导出接口,用import引入模块。但是在node模块中,使用module.exports/exports导出接口,使用require引入模块。
4.4.1 出现的时间不同
- require表示的是运行时加载/调用,所以理论上可以运用在代码的任何地方。
- import表示的是编译时加载(效率更高),由于是编译时加载,所以import命令会提升到整个模块的头部。
- import输入的变量是只读的,引用类型可以,其他模块也可以读到改后的值,但不建议修改。
- import是静态执行,不能使用表达式和变量。
// 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module;
- import是Singleton模式。
import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module'; // 虽然foo和bar在两个语句加载,但是他们对应的是同一个my_module实例
4.4.2 遵循的模块化规范不同
- require是AMD规范引入方式。
- import是ES6的一个语法标准,如果要兼容浏览器的话必须转化成ES5的语法。
4.4.3 本质
- require是赋值过程。module.exports后面的内容是什么,require的结果就是什么,比如对象、数字、字符串、函数等,然后再把require的结果赋值给某个变量,它相当于module.exports的传送门。
- import是结构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require。
- import虽然是es6中的语法,但就目前来说,所有的引擎都还没有实现import。
- import语法实际上会被转码为require。这也是为什么在模块导出时使用module.exports,在引入模板时使用import仍然起效,因为本质上,import会被转码为require去执行。