NodeJS——CommonJS模块规范

前言

最近一直在写大论文,前端的知识看得有点少,抽空看一看,以免生疏。本篇主要介绍关于NodeJS中基于CommonJS规范的模块,文中大量内容摘取自阮一峰廖雪峰两位大神的博客,整合加入了一些个人理解。

NodeJS中的模块

模块对外暴露变量:

1
module.exports = variable;

模块引用其他模块暴露的变量:

1
var ref = require('module_name');

CommonJS模块规范

基本内容

每个JS文件都是一个模块,内部各自使用的变量名和函数名互不冲突,包括全局变量。变量为当前模块的私有变量。

若想在多个文件分享变量,需定义为global对象的属性。

1
global.x = 'global';

一般不推荐全局写法。

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外输出的接口。其他文件加载某个模块,其实是加载该模块的module.exports属性。

Node保存了所有导入的module,使用require()获取module时,Node找到对应的module,将改module的exports变量返回,从而获得模块输出。

加载规则

require命令用于加载文件,后缀名默认为.js,可省略。

1
2
3
var greet = require('./hello'); //使用相对路径
var greet2 = require('/home/hello'); //使用绝对路径
var greet3 = require('hello'); //使用模块

参数字符串不是绝对路径和相对路径时,模块将按照下列查找顺序进行查找:内置模块、全局模块、当前模块。如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再进行搜索。

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

若想获得require命令加载的确切文件名,使用require.resolve()命令。

特性

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。
  • 模块中输入的变量是被输出模块的值的拷贝。即一旦输出一个值,模块内部的变化就不再影响该值。

模块实现原理

在浏览器中,不同JS文件中的同名全局变量会互相影响。JS语言本身没有模块机制保证不同模块可以使用相同变量名。

解决办法:使用闭包,将JS代码用一个函数包装,是该段代码的所有“全局变量”成为函数内的局部变量。

例:

1
2
var a = 'a';
console.log(a);

修改为:

1
2
3
4
(function(){
var a = 'a';
console.log(a);
})();

NodeJS就是利用JS语言函数式编程的特性,实现了模块的隔离。

module.exports vs exports

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令: var exports = module.exports;

1
2
3
4
5
6
7
8
9
//写法1
module.exports = {
a: a,
b: b
};
//写法2
exports.a = a;
exports.b = b;

不能直接对exports赋值,指向其他变量,因为Node提供的exports和module.exports实际是同一个变量,并且初始化为空对象{}。当对exports直接赋值时,等于切断了exports与module.exports的联系。

因此,如果要输出的是一个函数或数组,只能给module.exports赋值,给exports赋值时无效的。

Module对象

Node内部提供一个Module构建函数。所有模块都是Module的实例。

每个模块内部,都有一个module对象,代表当前模块。包括以下属性:

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。

模块缓存

所有缓存的模块保存在require.cache之中,

删除模块的缓存:

1
2
3
4
5
6
7
//删除指定模块缓存
deleterequire.cache[moduleName];
//删除所有模块的缓存
Object.keys(require.cache).forEach(function(key){
delete require.cache[key];
});

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。

模块的循环加载

如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
// a.js
module.exports.x = 'a1';
console.log('a.js', require('./b.js').x);
module.exports.x = 'a2';
// b.js
module.exports.x = 'b1';
console.log('b.js', require('./a.js').x);
module.exports.x = 'b2';
// main.js
console.log('main.js', require('./a.js').x);
console.log('main.js', require('./b.js').x);

执行结果如下:

1
2
3
4
5
$ node main.js
b.js a1 //模块b加载了模块a的不完整版本,即进入循环前的部分,个人理解为已缓存了a的exports部分,没有重新执行模块a代码,故不会重复console.log语句。
a.js b2 //模块b执行完毕,模块b的exports中的x值为b2
main.js a2 //继续执行广域网模块a的剩余部分,模块a的exports中的x值为a2
main.js b2 //第二次执行时,使用了缓存中的模块b的exports

未完待续

参考文章