CommonJS规范
概述
Node.js是基于CommonJS模块规范的实现,每个文件就是一个模块,有自己的作用域,模块内代码的写法必须遵守CommonJS规范。文件里定义的变量、函数、类,都是私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为global对象的属性。
global.x = 5;
这样变量x就可以被所有文件读取。但是,并不推荐这样写。
CommonJS模块的特点:
-
所有代码都运行在模块作用域,不会污染全局;
-
模块可多次加载,但只在第一次加载时运行一次,运行结果被缓存,以后加载直接读取缓存结果。再次运行,必须清除缓存;
-
模块加载顺序,按照在代码中的出现顺序。
module对象
Node内部提供一个Module
构建函数,所有模块都是它的实例。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
}
CommonJS规范规定,每个模块内部都有一个module对象,代表当前模块。它的exports
属性是对外的接口。加载一个模块,其实是加载该模块的module.exports
属性。
var x = 5;
var add = function (val) {
return val + x;
};
module.exports.x = x;
module.exports.add = add;
上面代码通过module.exports输出变量x和函数add。
require
方法用于加载模块。
var example = require('./example.js');
console.log(example.x);
console.log(example.add);
module对象的属性:
module.id 模块的识别符,通常是带有绝对路径的模块文件名;
module.filename 模块的文件名,带有绝对路径;
module.loaded 返回一个布尔值,表示模块是否完成加载;
module.parent 返回一个对象,表示调用该模块的模块;
module.children 返回一个数组,表示该模块要用到的其他模块;
module.exports 表示模块对外输出的值。
在命令行下调用某个模块,比如:node something.js
,那么module.parent
就是null。如果在脚本中调用,比如:require('./something.js')
,那么,module.parent
就是调用它的模块。利用这点,可以判断当前模块是否为入口脚本。
if(!module.parent) {
app.listen(8088, function() {
console.log('app listening on port 8088');
})
} else {
module.exports = app;
}
module.exports属性
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上是读取module.exports变量。
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
上面模块会在加载1秒后,发出ready事件。其他文件监听该事件,如下:
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
exports变量
Node为每个模块提供了一个exports变量,指向module.exports
。等于在每个模块头部加了一行命令:
var exports = module.exports;
在对外输出模块接口时,可以向exports对象添加方法:
exports.area = function (r) {
return Math.PI * r * r;
};
注意,不能直接给exports赋值,这样等于切断了exports与module.exports的联系。例如:
exports = function (x) {console.log(x)};
上面的写法,exports将不再指向module.exports。
如果,一个模块的对外接口,是一个单一的值,不能使用exports输出,只能使用module.exports。
module.exports = 'Hello world';
如果处理不好他们的区别,可以只使用module.exports。
AMD规范与CommonJS规范的兼容
CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作。AMD规范是非同步加载模块,允许指定回调函数。Node.js主要用于服务器编程,模块文件一般都存在于本地硬盘,所以加载比较快,不用考虑非同步加载方式,比较适用CommonJS规范。浏览器环境,需要从服务器端加载模块,就必须采用非同步模式,所以浏览器端一般采用AMD规范。
AMD规范使用define方法定义模块:
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
AMD规范允许输出的模块兼容CommonJS规范:
define(function(require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});
require命令
Node内置的require命令用于加载模块文件。
require命令的基本功能,读入并执行一个js文件,然后返回该模块的exports对象。如果没有发现指定模块会报错。
// example.js
var invisible = function() {
console.log("invisible");
}
exports.message = "hi";
exports.say = function() {
console.log(message);
}
输出exports对象:
var example = require('./example.js');
example
// 输出
// {
// message: "hi";
// say: [Function]
// }
如果模块输出的是一个函数,就不能定义在exports对象中,要定义在module.exports里面。
module.exports = function() {
console.log("hello world")
}
require('./example2/js')()
require命令调用自身,等于执行module.exports。
加载规则
require命令用于加载文件,后缀默认为.js
。
根据参数的不同格式,require命令去不同路径寻找模块文件。
(1)如果参数以"/"开头,表示加载的是一个绝对路径。
(2)如果参数以"./"开头,表示加载的是一个相对路径。
(3)如果参数不以以上两种开头,表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者位于node_modules目录的已安装模块(全局或局部安装)。
(4)如果参数不以"./"或"/"开头,而且是一个路径,如
require('example/path/file')
,则先找到example的位置,然后再以它为参数,找到后续路径。(5)如果指定的模块文件没有发现,Node会尝试为文件名加
.js
、.json
、.node
后,再搜索。.js
文件会以文本格式的js脚本文件解析,.json
文件会以JSON格式的文本文件解析,.node
文件会以编译后的二进制文件解析。(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。
举例:
执行require('bar.js')
命令,Node会依次搜索一下文件:
/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
目录的加载规则
通常,我们会把相关文件放在同一个目录里,便于组织管理。最好为该目录设置一个入口文件,require方法可以通过入口文件,加载整个目录。
在目录中放一个package.json
文件,并将入口文件写入main
字段。
{
"name": "some-library",
"main": "./lib/some-library.js"
}
require
发现参数是一个目录后,自动查看该目录的package.json
文件,然后加载main
字段指定的入口文件。如果package,json
文件没有main
字段或者没有package.json
文件,则加载该目录下的index.js
或index.node
文件。
模块的缓存
第一次加载模块时,Node会缓存该模块。以后再加载该模块,直接从缓存取出该模块的module.exports
属性。
如果要多次执行模块,可以让该模块输出一个函数,每次require这个模块的时候,重新执行输出的函数。
所有缓存的模块保存在require.cache
中,删除模块缓存:
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
注意,缓存是根据绝对路径识别模块的,如果同样的模块名,保存在不同路径,require命令还是会重新加载该模块。
环境变量NODE_PATH
Node执行一个脚本时,会先查看环境变量,它是一组以冒号分隔的绝对路径。在其他位置找不到指定模块时,Node会去这些路径查找。
可以将NODE_PATH添加到.bashrc
。
export NODE_PATH="/usr/local/lib/node"
如果遇到复杂的相对路径,如:"../../../../lib/myModule"
,可以将该文件加入node_modules
目录,还可以修改NODE_PATH
环境变量,package.json
文件可以用下面的写法:
{
"name": "node_path",
"version": "1.0.0",
"description": "",
"main": "index,js",
"scripts": {
"start": "NODE_PATH=lib node index.js"
},
"author": ""
}
NODE_PATH环境变量是历史遗留下来的路径解决方案,通常不应该使用,应该使用node_modules
目录。
模块的循环加载
模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。
// a.js
exports.x = 'a1';
console.log('a.js', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js', require('./a.js').x);
console.log('main.js', require('./b.js').x);
上面代码是三个js文件,a.js加载了b.js,b.js又加载了a.js,这时,Node返回a.js的不完整版本,所以,main.js中两次console.log输出的结果是不一样的。
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
第一次执行main.js
的require('./a.js').x
时,先执行a.js
,然后加载b.js
,执行b.js
时,a.js
的exports.x
等于a1,b.js
执行完成,所以exports.x
等于b2,第二次执行console.log('main.js', require('./b.js').x)
时,直接从缓存中读取exports属性,所以直接输出main.js b2
,a.js和b.js内部的console.log语句都不会执行。
require.amin
require方法有一个main属性,用来判断模块是直接执行,还是被调用执行(通过require加载该脚本执行)。直接执行的时候(node module.js
),require.main
属性指向模块本身require.main === module
结果为true
。
模块的加载机制
CommonJS模块的加载机制是,输出一个值,模块内部的变化就影响不到这个值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter
};
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
上面代码说明,counter输出以后,模块内部的变化影响不到counter。
require的内部处理流程
require
命令不是一个全局命令,而是指向当前模块的module.require
命令,module.require
调用Node的内部命令Module._load
。
Module._load = function(request, parent, isMain) {
// 1. 检查 Module._cache,是否缓存之中有指定模块
// 2. 如果缓存之中没有,就创建一个新的Module实例
// 3. 将它保存到缓存
// 4. 使用 module.load() 加载指定的模块文件,读取文件内容之后,使用 module.compile() 执行文件代码
// 5. 如果加载/解析过程报错,就从缓存删除该模块
// 6. 返回该模块的 module.exports
};
第4步,采用module.compile()
执行指定模块的脚本,逻辑如下:
Module.prototype._compile = function(content, filename) {
// 1. 生成一个require函数,指向module.require
// 2. 加载其他辅助方法到require
// 3. 将文件内容放到一个函数之中,该函数可调用 require
// 4. 执行该函数
};
require
函数及其辅助方法主要如下:
require(): 加载外部模块
require.resolve():将模块名解析到一个绝对路径
require.main:指向主模块
require.cache:指向所有缓存的模块
require.extensions:根据文件的后缀名,调用不同的执行函数
require
函数准备完毕,所有要加载的脚本内容,被放到一个新的函数中,这样可以避免污染全局环境。该函数的参数包括require
、module
、exports
,以及其他一些参数。
(function (exports, require, module, __filename, __dirname) {});
Module._compile
方法是同步执行的,所以Module._load
要等它执行完成,才会向用户返回module.exports
的值。
参考链接
本文参考阮一峰老师的文章(CommonJS规范),做了一些改动和总结。仅供学习和方便查阅。