Nodejs 的 CommonJS 规范实现原理( 二 )


node 模块的运行机制也很简单,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就可以实现代码间的作用域隔离 。
我们先在一个 js 文件中直接打印 arguments,得到的结果如下图所示,我们先记住这些参数 。
console.log(arguments) // exports, require, module, __filename, __dirname

Nodejs 的 CommonJS 规范实现原理

文章插图
Node 中通过 modules.export 导出 , require 引入 。其中 require 依赖 node 中的 fs 模块来加载模块文件,通过 fs.readFile 读取到的是一个字符串 。
在 javascrpt 中可以通过 eval 或者 new Function 的方式来将一个字符串转换成 js 代码来运行 。但是前面提到过,他们都有一个致命的问题,就是变量的污染 。
实现 require 模块加载器首先导入依赖的模块 path , fs,vm, 并且创建一个 Require 函数,这个函数接收一个 modulePath 参数 , 表示要导入的文件路径
const path = require('path');
const fs = require('fs');
const vm = require('vm');
// 定义导入类,参数为模块路径
function Require(modulePath) {
}
在 Require 中获取到模块的绝对路径,使用 fs 加载模块,这里读取模块内容使用 new Module 来抽象,使用 tryModuleLoad 来加载模块内容,Module 和 tryModuleLoad 稍后实现,Require 的返回值应该是模块的内容,也就是 module.exports 。
// 定义导入类 , 参数为模块路径
function Require(modulePath) {
// 获取当前要加载的绝对路径
let absPathname = path.resolve(__dirname, modulePath);
// 创建模块,新建Module实例
const module = new Module(absPathname);
// 加载当前模块
tryModuleLoad(module);
// 返回exports对象
return module.exports;
}
Module 的实现就是给模块创建一个 exports 对象,tryModuleLoad 执行的时候将内容加入到 exports 中,id 就是模块的绝对路径 。
// 定义模块, 添加文件id标识和exports属性
function Module(id) {
this.id = id;
// 读取到的文件内容会放在exports中
this.exports = {};
}
node 模块是运行在一个函数中,这里给 Module 挂载静态属性 wrApper,里面定义一下这个函数的字符串,wrapper 是一个数组,数组的第一个元素就是函数的参数部分,其中有 exports,module,Require,__dirname,__filename, 都是模块中常用的全局变量.
第二个参数就是函数的结束部分 。两部分都是字符串,使用的时候将他们包裹在模块的字符串外部就可以了 。
// 定义包裹模块内容的函数
Module.wrapper = [
"(function(exports, module, Require, __dirname, __filename) {",
"})"
]
_extensions 用于针对不同的模块扩展名使用不同的加载方式,比如 JSON 和 javascript 加载方式肯定是不同的 。JSON 使用 JSON.parse 来运行 。
javascript 使用 vm.runInThisContext 来运行,可以看到 fs.readFileSync 传入的是 module.id 也就是 Module 定义时候 id 存储的是模块的绝对路径,读取到的 content 是一个字符串,使用 Module.wrapper 来包裹一下就相当于在这个模块外部又包裹了一个函数,也就实现了私有作用域 。
使用 call 来执行 fn 函数,第一个参数改变运行的 this 传入 module.exports,后面的参数就是函数外面包裹参数 exports, module, Require, __dirname, __filename 。/
// 定义扩展名,不同的扩展名,加载方式不同,实现js和json
Module._extensions = {
'.js'(module) {
const content = fs.readFileSync(module.id, 'utf8');
const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
const fn = vm.runInThisContext(fnStr);
fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
},
'.json'(module) {
const json = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(json); // 把文件的结果放在exports属性上
}
}
tryModuleLoad 函数接收的是模块对象 , 通过 path.extname 来获取模块的后缀名 , 然后使用 Module._extensions 来加载模块 。
// 定义模块加载方法
function tryModuleLoad(module) {
// 获取扩展名
const extension = path.extname(module.id);
// 通过后缀加载当前模块
Module._extensions[extension](module); // 策略模式???
}
到此 Require 加载机制基本就写完了 。Require 加载模块的时候传入模块名称,在 Require 方法中使用 path.resolve (__dirname, modulePath) 获取到文件的绝对路径 。然后通过 new Module 实例化的方式创建 module 对象,将模块的绝对路径存储在 module 的 id 属性中,在 module 中创建 exports 属性为一个 json 对象 。


推荐阅读