NodeJS中require,module,exports的关系

前言

作为NodeJS的开发人员,想必都知道NodeJS主要通过require,exports这两个关键字将代码中的各个模块组合到一起。其中具体的机制,之前也大致看过,但是一直没有完整的整理过一遍。本文就从运行一个NodeJS脚本开始,把NodeJS加载脚本的过程梳理一遍,算是当作自己的笔记吧。

记录

准备

我这边的NodeJS版本是

1
2
$ node -v
v5.3.0

编写一个test.js文件。是的,里面就写这一句话。

1
require()

执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ node test.js

assert.js:89
throw new assert.AssertionError({
^
AssertionError: missing path
at Module.require (module.js:352:3)
at require (internal/module.js:12:17)
at Object.<anonymous> (f:\work\test\nodejs\test.js:1:63)
at Module._compile (module.js:398:26)
at Object.Module._extensions..js (module.js:405:10)
at Module.load (module.js:344:32)
at Function.Module._load (module.js:301:12)
at Function.Module.runMain (module.js:430:10)
at startup (node.js:141:18)
at node.js:980:3

堆栈解析

这里出错的堆栈信息还是蛮有意思的,我把主要的代码都罗列了下。

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
36
37
38
// NodeJS通过startup方法初始化,然后进入了Module的runMain方法
Module.runMain = function() {
//加载命令行中的第二个参数所指向的“test.js”模块
Module._load(process.argv[1], null, true);
};

Module.prototype.load = function(filename) {
// 根据文件名的后缀调用具体的解析方式
Module._extensions[extension](this, filename);
};

Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
// 在读取完内容js文件内容之后进行编译
module._compile(internalModule.stripBOM(content), filename);
};

/*
以上三个方法只是解析 node test.js 这个命令,然后找到了 test.js 这个文件而已。
也就是对应着堆栈里面的:
at Object.Module._extensions..js (module.js:405:10)
at Module.load (module.js:344:32)
at Function.Module._load (module.js:301:12)
at Function.Module.runMain (module.js:430:10)
at startup (node.js:141:18)
at node.js:980:3

下面的_compile才是重点!
*/

Module.prototype._compile = function(content, filename) {
.....
const dirname = path.dirname(filename);
const require = internalModule.makeRequireFunction.call(this);
// 这里是重点!!!NodeJS在所有文件中的代码完成,都嵌套了一层。
const args = [this.exports, require, this, filename, dirname];
return compiledWrapper.apply(this.exports, args);
};

NodeJS的包裹层!

通过这个包裹层,我们原先的test.js文件就会变为下面这个样子。这也就是在我们写代码的时候,可以直接调用exports,require,module…的原因。

1
2
3
(function(exports, require, module, filename, dirname)){
require()
}
  1. module
    test.js会被解析为一个module。
  2. exports
    就是这个module上的一个属性。
  3. require
    调用require方法,最终调用的就是Module._load方法
    1
    2
    3
    4
    5
    Module.prototype.require = function(path) {
    assert(path, 'missing path');
    assert(typeof path === 'string', 'path must be a string');
    return Module._load(path, this);
    };

最终的那个异常

在require方法中,NodeJS会对参数进行检查。这也就是当我们最终执行test.js时,出现错误“AssertionError: missing path”的原因所在。

小结

所有加载完成的模块,都会缓存在require.cache上。当我们执行require操作的时候,NodeJS会先从缓存里面找,如果不存在,就会去找对应的文件进行编译。
这一个特性可以让我们做不少有趣的事情,比如:

  1. 文件实时编译。
    通过检测文件的改动,在发生改动时,清除require.cache缓存,对其进行重新加载编译。
  2. 动态的往exports上添加方法、属性。
    因为exports是module对象上的属性,而module对象又是被缓存在require.cache上,所以也可以这样写。
    1
    2
    3
    exports.addMethod = function(methodName , method){
    exports[methodName] = method
    }

转载本站文章请注明作者(xtutu)和出处 xtutu