CommonJS 是一种模块化的标准,而 NodeJS 是这种标准的实现,每一个文件就是一个模块,有本身的做用域。在一个文件里面定义的变量、函数、类,都是私有的,对其余文件不可见。javascript
在实现模块加载以前,咱们须要清除模块的加载过程:java
A
文件夹下有一个 a.js
,咱们要解析出一个绝对路径来;.js
、.json
;由于咱们只是实现 CommonJS 的模块加载方法,并不会去实现整个 Node,在这里咱们须要依赖一些 Node 的模块,因此咱们就 “不要脸” 的使用 Node 自带的 require
方法把模块加载进来。json
1
2
3
4
5
6
7
8
复制代码 |
// 操做文件的模块
const fs = require("fs");
// 处理路径的模块
const path = require("path");
// 虚拟机,帮咱们建立一个黑箱执行代码,防止变量污染
const vm = require("vm");
复制代码 |
其实 CommonJS 中引入的每个模块咱们都须要经过 Module
构造函数建立一个实例。数组
1
2
3
4
5
6
7
8
复制代码 |
/* * @param {String} p */
function Module(p) {
this.id = p; // 当前文件的表示(绝对路径)
this.exports = {}; // 每一个模块都有一个 exports 属性,用来存储模块的内容
this.loaded = false; // 标记是否被加载过
}
复制代码 |
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() {}
};
复制代码 |
为了防止和 Node 自带的 require
方法重名,咱们将模拟的方法重命名为 req
。缓存
1
2
3
4
5
6
7
8
9
10
复制代码 |
/* * @param {String} moduleId */
function req(moduleId) {
// 将 req 传入的参数处理成绝对路径
let p = Module._resolveFileName(moduleId);
// 生成一个新的模块
let module = new Module(p);
}
复制代码 |
在上面代码中,咱们先把传入的参数经过 Module._resolveFileName
处理成了一个绝对路径,并建立模块实例把绝对路径做为参数传入,咱们如今实现一下 Module._resolveFileName
方法。bash
这个方法的功能就是将 req
方法的参数根据是否有后缀名两种方式处理成带后缀名的文件绝对路径,若是 req
的参数没有后缀名,会去按照 Module._extensions
的键的后缀名顺序进行查找文件,直到找到后缀名对应文件的绝对路径,优先 .js
,而后是 .json
,这里咱们只实现这两种文件类型的处理。闭包
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
复制代码 |
/* * @param {String} moduleId */
Module._resolveFileName = function(moduleId) {
// 将参数拼接成绝对路径
let p = path.resolve(moduleId);
// 判断是否含有后缀名
if (!/\.\w+$/.test(p)) {
// 建立规范规定查找文件后缀名顺序的数组 .js .json
let arr = Object.keys(Module._extensions);
// 循环查找
for (let i = 0; i < arr.length; i++) {
// 将绝对路径与后缀名进行拼接
let file = p + arr[i];
// 查找不到文件时捕获异常
try {
// 并经过 fs 模块同步查找文件的方法对改路径进行查找,文件未找到会直接进入 catch 语句
fs.accessSync(file);
// 若是找到文件将该文件绝对路径返回
return file;
} catch (e) {
// 当后缀名循环完毕都没有找到对应文件时,抛出异常
if (i >= arr.length) throw new Error("not found module");
}
}
} else {
// 有后缀名直接返回该绝对路径
return p;
}
};
复制代码 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码 |
/* * @param {String} moduleId */
function req(moduleId) {
// 将 req 传入的参数处理成绝对路径
let p = Module._resolveFileName(moduleId);
// 生成一个新的模块
let module = new Module(p);
// ********** 下面为新增代码 **********
// 加载模块
let content = module.load(p);
// 将加载后返回的内容赋值给模块实例的 exports 属性上
module.exports = content;
// 最后返回 模块实例的 exports 属性,即加载模块的内容
return module.exports;
// ********** 上面为新增代码 **********
}
复制代码 |
上面代码实现了一个实例方法 load
,传入文件的绝对路径,为模块加载文件的内容,在加载后将值存入模块实例的 exports
属性上最后返回,其实 req
函数返回的就是模块加载回来的内容。app
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;
};
复制代码 |
还记得前面准备的静态属性中有 Module._extensions
就是用来存储这两个方法的,下面咱们来完善这两个方法。svg
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) {
// 读取 js 文件,返回文件的内容
let script = fs.readFileSync(module.id, "utf8");
// 给 js 文件的内容增长一个闭包环境
let fn = Module.wrap(script);
// 建立虚拟机,将咱们建立的 js 函数执行,将 this 指向模块实例的 exports 属性
vm.runInThisContext(fn).call(
module.exports,
module.exports,
req,
module
);
// 返回模块实例上的 exports 属性(即模块的内容)
return module.exports;
},
".json": function(module) {
// .json 文件的处理相对简单,将读出的字符串转换成对象便可
return JSON.parse(fs.readFileSync(module.id, "utf8"));
}
};
复制代码 |
咱们这里使用了 Module.wrap
方法,代码以下,其实帮助咱们加了一个闭包环境(即套了一层函数并传入了咱们须要的参数),里面全部的变量都是私有的。模块化
1
2
3
复制代码 |
Module.wrap = function(content) {
return Module.wrapper[0] + content + Module.wrapper[1];
};
复制代码 |
Module.wrapper
的两个值其实就是咱们须要在外层包了一个函数的前半段和后半段。
这里咱们要划重点了,很是重要:
一、咱们在虚拟机中执行构建的闭包函数时利用执行上/下文 call
将 this
指向了模块实例的 exports
属性上,因此这也是为何咱们用 Node 启动一个 js
文件,打印 this
时,不是全局对象 global
,而是一个空对象,这个空对象就是咱们的 module.exports
,即当前模块实例的 exports
属性。
二、仍是第一条的函数执行,咱们传入的第一个参数是改变 this
指向,那第二个参数是 module.exports
,因此在每一个模块导出的时候,使用 module.exports = xxx
,其实直接替换了模块实例的值,即直接把模块的内容存放在了模块实例的 exports
属性上,而 req
最后返回的就是咱们模块导出的内容。
三、第三个参数之因此传入 req
是由于咱们还可能在一个模块中导入其余模块,而 req
会返回其余模块的导出在当前模块使用,这样整个 CommonJS 的规则就这样创建起来了。
咱们如今的程序是有问题的,当重复加载了一个已经加载过得模块,当执行 req
方法的时候会发现,又建立了一个新的模块实例,这是不合理的,因此咱们下面来实现一下缓存机制。
还记得以前的一个静态属性 Module._cacheModule
,它的值是一个空对象,咱们会把全部加载过的模块的实例存储到这个对象上。
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
复制代码 |
/* * @param {String} moduleId */
function req(moduleId) {
// 将 req 传入的参数处理成绝对路径
let p = Module._resolveFileName(moduleId);
// ********** 下面为新增代码 **********
// 判断是否已经加载过
if (Module._cacheModule[p]) {
// 模块存在,若是有直接把 exports 对象返回便可
return Module._cacheModule[p].exprots;
}
// ********** 上面为新增代码 **********
// 生成一个新的模块
let module = new Module(p);
// 加载模块
let content = module.load(p);
// ********** 下面为新增代码 **********
// 存储时是拿模块的绝对路径做为键与模块内容相对应的
Module._cacheModule[p] = module;
// 是否缓存表示改成 true
module.loaded = true;
// ********** 上面为新增代码 **********
// 将加载后返回的内容赋值给模块实例的 exports 属性上
module.exports = content;
// 最后返回 模块实例的 exports 属性,即加载模块的内容
return module.exports;
}
复制代码 |
在同级目录下新建一个文件 a.js
,使用 module.exports
随便导出一些内容,在咱们实现模块加载的最下方尝试引入并打印内容。
1
2
复制代码 |
// a.js
module.exports = "Hello world";
复制代码 |
1
2
复制代码 |
const a = req("./a");
console.log(a); // Hello world
复制代码 |
其实咱们只实现了 CommonJS 规范的一部分,即自定义模块的加载,其实在 CommonJS 的规范当中关于模块查找的规则还有不少,具体的咱们就用下面的流程图来表示。
这篇文章让咱们了解了 CommonJS 是什么,主要目的在于理解 Node 模块化的实现思路,想要更深刻的了解 CommonJS 的实现细节,建议看一看 NodeJS 源码对应的部分,若是以为源码比较多,不容易找到模块化实现的代码,也能够在 VSCode 中经过调用 require
方法引入模块时,打断点调试,一步一步的跟进到 Node 源码中查看。