深刻理解CommonJS规范

59次阅读

共计 4321 个字符,预计需要花费 11 分钟才能阅读完成。

node 采用的是 CommonJS 规范。每一个文件就是一个单独的模块,拥有属于自身的独立作用域,变量以及方法等。这些对其他模块都是不可见的。CommonJS 规范规定,每个模块内部,module 代表当前模块。module 是一个对象,它有一个 exports 属性,也就是 module.exports。该属性是对外的接口,把需要导出的内容放到该属性上。外部可以通过 require 进行导入。require 导入的就是 exports 中的内容。
该篇文章就手动实现以下 require 方法,通过手写的 require 方法拿到另一个文件中的 exports 中的内容。
首先,我们先看一下 node 环境中标准的 require 方法是如何引用模块的。新建文件夹,在文件夹中新建 b.js。通过 module.exports 将内容导出。b.js:
let str = ‘b.js 导出的内容 ’;
module.exports = str;
然后新建另一个文件,my-require.js。在 my-require.js 中引入 b.js 中的 str。my-require.js:
let str = require(‘./b.js’);

console.log(str);
运行代码,可以看到。打印出了 b 的内容:b.js 导出的内容。以上是标准 CommonJS 中 require 的引用,接下来手动实现它:首先梳理以下逻辑,require 函数中传递的参数是一个路径,有路径再加上 node 的 fs 模块,我们就可以读取到该文件。那有了该文件的内容,从该文件中获取 exports 就不是什么难事了。上代码:

let path = require(‘path’);
let fs = require(‘fs’);
let vm = require(‘vm’);

/** 定义自己的 require 方法 myrequire() */
function myrequire(modulePath){
let absPath = path.resolve(__dirname,modulePath);
function find(absPath){
try{
fs.accessSync(absPath);
return absPath;
}catch(e){
console.log(e);
}
}
absPath = find(absPath);
let module = new Module(absPath);
loadModule(module);
return module.exports;
}

function Module(id){
this.id = id;
this.exports = {}
}

function loadModule(module){
let extension = path.extname(module.id);
Module._extensions[extension](module);
}

Module._extensions = {
‘.js'(module){
let content = fs.readFileSync(module.id, ‘utf8’);
let fnStr = Module.wrapper[0]+content+Module.wrapper[1];
let fn = vm.runInThisContext(fnStr);
fn.call(module.exports,module.exports,module,myrequire);
}
}

Module.wrapper = [
‘(function(exports,module,require,__dirname,__dirname){‘,
‘})’
];

let str = myrequire(‘./b.js’);

console.log(str);
阅读顺序从上至下。首先 引入了 path fs 和 vm 模块。path 和 fs 都不用说了,都懂。vm 模块是 node 的核心模块。核心功能官方解释的是:
The vm module provides APIs for compiling and running code within V8 Virtual Machine contexts. The vm module is not a security mechanism. Do not use it to run untrusted code. The term “sandbox” is used throughout these docs simply to refer to a separate context, and does not confer any security guarantees.
意思大致是:vm 可以使用 v8 的 Virtual Machine contexts 动态地编译和执行代码,而代码的执行上下文是与当前进程隔离的,但是这里的隔离并不是绝对的安全,不完全等同浏览器的沙箱环境。其实 vm 模块在该本文中的作用就是执行字符串代码,这样理解就好。
首先,定义了一个 myrequire 的方法。该方法传入一个相对路径。在 myrequire 方法中第一步将相对路径转换为绝对路径。然后又通过一个 find 方法来校验该路径是否存在。接下来通过构造函数 Module 传入绝对路径,new 出了实例 module。该构造函数 Module 传入了路径 id,内部定义了属性 exports={}。该属性就是文件导出的属性。
紧接着,通过 loadModule 方法传入了实例 module,来加载该文件。在 loadModule 方法中,首先获取了文件名后缀.js。把文件名后缀.js 传给 Module._extensions。在 Module._extensions 对象中,通过文件后缀名.js 找到该文件类型的解析方法。并把实例 module 传递进去。在该方法中,通过 module.id 路径和 fs 模块通过获取到该文件内容 content。注意下一步。在该文件内容 content 的外面用 (function(exports,modules,require,__dirname,__filename){}) 函数包裹了一层。这样做的目的是待会要执行该函数并且拿到其中的 module.exports 中导出的内容。但是我们刚才通过 fs 读取到的文件内容仅仅是字符串,又包裹了一层空函数,还是字符串。接下来就要用到 vm 模块。该模块可以执行字符串代码。通过 vm.runInthisContext()方法,将刚才得到的字符串传递进去。此时就得到了可以执行的方法 fn。那接下来就是执行该方法 fn 了。执行 fn,把刚才的参数传递进去。注意当前 this 执行为 module.exports。这样才能拿到 module.exports 中的内容。最后在 myrequire 中末尾,返回了该 exports 内容。return module.exports。好,接下来就是验证效果了。右键 code run,或者浏览器中打开。可以看到:
b.js 导出的内容
拿到了文件 b.js 中的内容,并且打印了出来。好,现在以及实现了最简单了 require。可是,我们并不满足于此。因为该 require 方法还有一些问题。比如说,还不能引用 json 文件,而且也没有考虑如果文件没有后缀的情况。接下来继续完善 myrequire 方法:

let path = require(‘path’);
let fs = require(‘fs’);
let vm = require(‘vm’);

/** 定义自己的 require 方法 myrequire() */
function myrequire(modulePath){
let absPath = path.resolve(__dirname,modulePath);
let ext_name = Object.keys(Module._extensions);
let index = 0;
let old_absPath = absPath;
function find(absPath){
try{
fs.accessSync(absPath);
return absPath;
}catch(e){
let ext = ext_name[index++];
let newPath = old_absPath+ext;
return find(newPath);
}
}
absPath = find(absPath);
let module = new Module(absPath);
loadModule(module);
return module.exports;
}

function Module(id){
this.id = id;
this.exports = {}
}

function loadModule(module){
let extension = path.extname(module.id);
Module._extensions[extension](module);
}

Module._extensions = {
‘.js'(module){
let content = fs.readFileSync(module.id, ‘utf8’);
let fnStr = Module.wrapper[0]+content+Module.wrapper[1];
let fn = vm.runInThisContext(fnStr);
fn.call(module.exports,module.exports,module,myrequire);
},
‘.json'(module){
let content = fs.readFileSync(module.id, ‘utf8’);
module.exports = content;
}
}

Module.wrapper = [
‘(function(exports,module,require,__dirname,__dirname){‘,
‘})’
];

let str = myrequire(‘./b’);

console.log(str);
console.log(myrequire(‘./a’));
在 myrequire 方法的第二行,先获取到 Module._extensions 中的所有后缀(目前有.js 和.json),又声明了一个下标 index,最后有保存了该路径 old_absPath。在 find 方法中,如果用户没有写文件后缀,就会自动拼接后缀。循环去查找,直到找到或者到最后也没找到。在 Module._extensions 中新增了一个对象.json 的方法。该方法较为简单。通过 fs 读取到文件并把文件内容放到 module.exports 中。ok,看下效果吧:
b.js 导出的内容
{
“name”:” 要引入的内容 ”
}
可以看到。正常拿到了 b.js 中的内容而且也读取到了 a.json 中的内容。至此,我们就实现了 CommonJS 中的 require 方法。写文章不易,喜欢就点个???? 吧 thx~

正文完
 0