CommonJS 概述
CommonJS 是一种模块化的标准,而 NodeJS 是这种标准的实现,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
NodeJS 模块化的简易实现
在实现模块加载之前,我们需要清除模块的加载过程:
- 假设
A
文件夹下有一个 a.js
,我们要解析出一个绝对路径来; - 我们写的路径可能没有后缀名
.js
、.json
; - 得到一个真实的加载路径(模块会被缓存)先去缓存中看一下这个文件是否存在,如果存在返回缓存 没有则创建一个模块;
- 得到对应文件的内容,加一个闭包,把内容塞进去,之后执行即可。
1、提前加载需要用到的模块
因为我们只是实现 CommonJS 的模块加载方法,并不会去实现整个 Node,在这里我们需要依赖一些 Node 的模块,所以我们就 “不要脸” 的使用 Node 自带的 require
方法把模块加载进来。
依赖模块
1
2
3
4
5
6
7
8
| // 操作文件的模块
const fs = require("fs");
// 处理路径的模块
const path = require("path");
// 虚拟机,帮我们创建一个黑箱执行代码,防止变量污染
const vm = require("vm");
|
2、创建 Module 构造函数
其实 CommonJS 中引入的每一个模块我们都需要通过 Module
构造函数创建一个实例。
创建 Module 构造函数
1
2
3
4
5
6
7
8
|
function Module(p) {
this.id = p;
this.exports = {};
this.loaded = false;
}
|
3、定义静态属性存储我们需要使用的一些值
Module 静态变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Module.wrapper = [
"(function (exports, require, module, __dirname, __filename) {",
"\n})"
];
Module._cacheModule = {};
Module._extensions = {
".js": function() {},
".json": function() {}
};
|
4、创建引入模块的 req 方法
为了防止和 Node 自带的 require
方法重名,我们将模拟的方法重命名为 req
。
引入模块方法 req
1
2
3
4
5
6
7
8
9
10
|
function req(moduleId) {
let p = Module._resolveFileName(moduleId);
let module = new Module(p);
}
|
在上面代码中,我们先把传入的参数通过 Module._resolveFileName
处理成了一个绝对路径,并创建模块实例把绝对路径作为参数传入,我们现在实现一下 Module._resolveFileName
方法。
5、返回文件绝对路径 Module._resolveFileName 方法的实现
这个方法的功能就是将 req
方法的参数根据是否有后缀名两种方式处理成带后缀名的文件绝对路径,如果 req
的参数没有后缀名,会去按照 Module._extensions
的键的后缀名顺序进行查找文件,直到找到后缀名对应文件的绝对路径,优先 .js
,然后是 .json
,这里我们只实现这两种文件类型的处理。
处理绝对路径 _resolveFileName 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
Module._resolveFileName = function(moduleId) {
let p = path.resolve(moduleId);
if (!/\.\w+$/.test(p)) {
let arr = Object.keys(Module._extensions);
for (let i = 0; i < arr.length; i++) {
let file = p + arr[i];
try {
fs.accessSync(file);
return file;
} catch (e) {
if (i >= arr.length) throw new Error("not found module");
}
}
} else {
return p;
}
};
|
6、加载模块的 load 方法
完善 req 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function req(moduleId) {
let p = Module._resolveFileName(moduleId);
let module = new Module(p);
let content = module.load(p);
module.exports = content;
return module.exports;
}
|
上面代码实现了一个实例方法 load
,传入文件的绝对路径,为模块加载文件的内容,在加载后将值存入模块实例的 exports
属性上最后返回,其实 req
函数返回的就是模块加载回来的内容。
load 方法
1
2
3
4
5
6
7
8
9
10
11
|
Module.prototype.load = function(filepath) {
let ext = path.extname(filepath);
let content = Moudule._extensions[ext](this);
return content;
};
|
7、实现加载 .js 文件和 .json 文件的方法
还记得前面准备的静态属性中有 Module._extensions
就是用来存储这两个方法的,下面我们来完善这两个方法。
处理后缀名方法的 _extensions 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| Module._extensions = {
".js": function(module) {
let script = fs.readFileSync(module.id, "utf8");
let fn = Module.wrap(script);
vm.runInThisContext(fn).call(
module.exports,
module.exports,
req,
module
);
return module.exports;
},
".json": function(module) {
return JSON.parse(fs.readFileSync(module.id, "utf8"));
}
};
|
我们这里使用了 Module.wrap
方法,代码如下,其实帮助我们加了一个闭包环境(即套了一层函数并传入了我们需要的参数),里面所有的变量都是私有的。
创建闭包 wrap 方法
1
2
3
| Module.wrap = function(content) {
return Module.wrapper[0] + content + Module.wrapper[1];
};
|
Module.wrapper
的两个值其实就是我们需要在外层包了一个函数的前半段和后半段。
这里我们要划重点了,非常重要:
1、我们在虚拟机中执行构建的闭包函数时利用执行上/下文 call
将 this
指向了模块实例的 exports
属性上,所以这也是为什么我们用 Node 启动一个 js
文件,打印 this
时,不是全局对象 global
,而是一个空对象,这个空对象就是我们的 module.exports
,即当前模块实例的 exports
属性。
2、还是第一条的函数执行,我们传入的第一个参数是改变 this
指向,那第二个参数是 module.exports
,所以在每个模块导出的时候,使用 module.exports = xxx
,其实直接替换了模块实例的值,即直接把模块的内容存放在了模块实例的 exports
属性上,而 req
最后返回的就是我们模块导出的内容。
3、第三个参数之所以传入 req
是因为我们还可能在一个模块中导入其他模块,而 req
会返回其他模块的导出在当前模块使用,这样整个 CommonJS 的规则就这样建立起来了。
8、对加载过的模块进行缓存
我们现在的程序是有问题的,当重复加载了一个已经加载过得模块,当执行 req
方法的时候会发现,又创建了一个新的模块实例,这是不合理的,所以我们下面来实现一下缓存机制。
还记得之前的一个静态属性 Module._cacheModule
,它的值是一个空对象,我们会把所有加载过的模块的实例存储到这个对象上。
完善 req 方法(处理缓存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
function req(moduleId) {
let p = Module._resolveFileName(moduleId);
if (Module._cacheModule[p]) {
return Module._cacheModule[p].exprots;
}
let module = new Module(p);
let content = module.load(p);
Module._cacheModule[p] = module;
module.loaded = true;
module.exports = content;
return module.exports;
}
|
9、试用 req 加载模块
在同级目录下新建一个文件 a.js
,使用 module.exports
随便导出一些内容,在我们实现模块加载的最下方尝试引入并打印内容。
导出自定义模块
1
2
|
module.exports = "Hello world";
|
检测 req 方法
1
2
| const a = req("./a");
console.log(a);
|
CommonJS 模块查找规范
其实我们只实现了 CommonJS 规范的一部分,即自定义模块的加载,其实在 CommonJS 的规范当中关于模块查找的规则还有很多,具体的我们就用下面的流程图来表示。

这篇文章让我们了解了 CommonJS 是什么,主要目的在于理解 Node 模块化的实现思路,想要更深入的了解 CommonJS 的实现细节,建议看一看 NodeJS 源码对应的部分,如果觉得源码比较多,不容易找到模块化实现的代码,也可以在 VSCode 中通过调用 require
方法引入模块时,打断点调试,一步一步的跟进到 Node 源码中查看。
原文出自:https://www.pandashen.com