Node.js(Node模块原理分析【详细】)
1、Node模块
1. 在CommonJS规范中一个文件就是一个模块。
2. 在CommonJS规范中通过exports暴露数据。
3. 在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虚拟机执行代码
- 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
- 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模块原理分析
在查看官方的实现原理是最好用低版本的node,然后用debug查看。
既然一个文件就是一个模块,既然想要使用模块必须先通过require()导入模块。所以可以推断出require()的作用其实就是读取文件。所以想要了解Node是如何实现模块的,必须先了解如何执行读取到的代码。
Node模块加载流程分析
-
内部实现了一个require方法。
function require(path) {
return self.require(path);
} -
通过Module对象的静态__load方法加载模块文件。
Module.prototype.require = function(path) {
return Module._load(path, this, /* isMain */ false);
}; -
通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名。
var filename = Module._resolveFilename(request, parent, isMain); -
根据路径判断是否有缓存, 如果没有就创建一个新的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 = {};
} -
利用tryModuleLoad方法加载模块tryModuleLoad(module, filename)。
5.1、取出模块后缀
var extension = path.extname(filename);
5.2、根据不同后缀查找不同方法并执行对应的方法, 加载模块
Module._extensions[extension](this, filename);
5.3、如果是JSON就转换成对象
module.exports = JSON.parse(internalModule.stripBOM(content));
5.4、如果是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});’
];
5.5、执行包裹函数之后的代码, 拿到执行结果(String – Function)
var compiledWrapper = vm.runInThisContext(wrapper);
5.6、利用call执行fn函数, 修改module.exports的值
var args = [this.exports, require, module, filename, dirname];
var result = compiledWrapper.call(this.exports, args);
5.7、返回module.exports
return module.exports;
自己实现一个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
}
附:call方法: