Node模块,CommonJS规范详解
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多语言都采用这种组织代码的方式。在node环境中,一个.js文件就称之为一个模块(module),有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。node 应用由模块组成,采用 commonjs 模块规范。1、使用模块有什么好处?最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括node内置的模块和来自第三方的模块。使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。
// example.js var x = 5; var addx = function (value) { return value + x; };
上面代码中,变量x和函数addx,是当前文件example.js私有的,其他文件不可见。如果想在多个文件分享变量,必须定义为global对象的属性。
global.warning = true;
2、模块的基本使用commonjs规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。'use strict';var s = 'hello';function greet(name) { console.log(s + ', ' + name + '!');}module.exports = greet;函数greet()是我们在hello模块中定义的,然后通过module.exports接口把函数greet作为模块的输出暴露出去,这样其他模块就可以使用greet函数了。require方法用于加载模块。我们再编写一个main.js文件,调用hello模块的greet函数:'use strict';// 引入hello模块:var greet = require('./hello');var s = 'michael';greet(s); // hello, michael!注意到引入hello模块用node提供的require函数:var greet = require('./hello');引入的模块作为变量保存在greet变量中,那greet变量到底是什么东西?其实变量greet就是在hello.js中我们用module.exports = greet;输出的greet函数。所以,main.js就成功地引用了hello.js模块中定义的greet()函数,接下来就可以直接使用它了。3、module对象node内部提供一个module构建函数。所有模块都是module的实例。
function module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; // ...
每个模块内部,都有一个module对象,代表当前模块。它有以下属性。
module.id模块的识别符,通常是带有绝对路径的模块文件名。
module.filename模块的文件名,带有绝对路径。
module.loaded返回一个布尔值,表示模块是否已经完成加载。
module.parent返回一个对象,表示调用该模块的模块。
module.children返回一个数组,表示该模块要用到的其他模块。
module.exports表示模块对外输出的值。例如:
// example.js var jquery = require('jquery'); exports.$ = jquery; console.log(module);
输出的module:
{ id: '.', exports: { '$': [function] }, parent: null, filename: '/path/to/example.js', loaded: false, children: [ { id: '/path/to/node_modules/jquery/dist/jquery.js', exports: [function], parent: [circular], filename: '/path/to/node_modules/jquery/dist/jquery.js', loaded: true, children: [], paths: [object] } ], paths: [ '/home/user/deleted/node_modules', '/home/user/node_modules', '/home/node_modules', '/node_modules' ] }
如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。
if (!module.parent) { // ran with `node something.js` app.listen(8088, function() { console.log('app listening on port 8088'); }) } else { // used with `require('/.something.js')` module.exports = app; }
module.exports属性module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。exports变量为了方便,node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
所以,在对外输出模块接口时,也可以向exports对象添加方法。
exports.area = function (r) { return math.pi * r * r; }; exports.circumference = function (r) { return 2 * math.pi * r; };
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
exports = function(x) {console.log(x)};
上面这样的写法是无效的,因为exports不再指向module.exports了。下面的写法也是无效的。
exports.hello = function() { return 'hello'; }; module.exports = 'hello world';
上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。4、require命令4.1 加载规则(详见node加载模块顺序)4.2 模块的缓存第一次加载某个模块时,node会缓存该模块。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个module对象,以后再加载该模块,就直接从缓存取出对应的module对象,然后获取module.exports属性。
{ //module对象 id: '...', exports: { ... }, loaded: true, ... }
即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,commonjs 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除缓存。所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。
// 删除指定模块的缓存 delete require.cache[modulename]; // 删除所有模块的缓存 object.keys(require.cache).foreach(function(key) { delete require.cache[key]; })
注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。4.3 require.mainrequire方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。直接执行的时候(node module.js),require.main属性指向模块本身。
require.main === module // true
调用执行的时候(通过require加载该脚本执行),上面的表达式返回false。5、模块的加载机制commonjs模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// lib.js var counter = 3; function inccounter() { counter++; } module.exports = { counter: counter, inccounter: inccounter, };
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.inccounter(); console.log(mod.counter); // 3
上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了(调用mod.inccounter()方法时,操作的是lib.js模块中的变量counter,而不是导出的变量counter)。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
// lib.js var counter = 3; function inccounter() { counter++; } module.exports = { get counter() { return counter }, inccounter: inccounter, };
上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。
上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。
5.1 require的内部处理流程require命令是commonjs规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的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
采用module.compile()执行指定模块的脚本,逻辑如下。
module.prototype._compile = function(content, filename) {};
在该函数中需要执行以下几步:1. 生成一个require函数,指向module.require;2. 加载其他辅助方法到require;3. 一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括require、module、exports,以及其他一些参数。
(function (exports, require, module, __filename, __dirname) { // your code injected here! // 执行读取的hello.js代码: function greet(name) { console.log('hello, ' + name + '!'); } module.exports = greet; // hello.js代码执行结束 });
4. 执行该函数上面的第1步和第2步,require函数及其辅助方法主要如下。
require(): 加载外部模块
require.resolve():将模块名解析到一个绝对路径
require.main:指向主模块
require.cache:指向所有缓存的模块
require.extensions:根据文件的后缀名,调用不同的执行函数module._compile方法是同步执行的,所以module._load要等它执行完成,才会向用户返回module.exports的值。5.2 模块的循环加载commonjs 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。我们来看一段代码:
// main.js var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// a.js exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕');
//b.js exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕');
当第一次require('a.js')时,执行a.js脚本。先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。a.js已经执行的部分,只有一行。exports.done = false;因此,对于b.js来说,它从a.js只输入一个变量done,值为false。然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。
最后输出的结果:
$ node main.js 在 b.js 之中,a.done = false b.js 执行完毕 在 a.js 之中,b.done = true a.js 执行完毕 在 main.js 之中, a.done=true, b.done=true
上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行
exports.done = true;
5.3 node.js是如何实现模块node实现“模块”功能的奥妙在于javascript是一种函数式编程语言,它支持闭包。如果我们把一段javascript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。请注意我们编写的hello.js代码是这样的:
var s = 'hello'; var name = 'world'; console.log(s + ' ' + name + '!');
node.js加载了hello.js后,它可以把代码包装一下,变成这样执行:
(function () { // 读取的hello.js代码: var s = 'hello'; var name = 'world'; console.log(s + ' ' + name + '!'); // hello.js代码结束 })();
这样一来,原来的全局变量s现在变成了匿名函数内部的局部变量。如果node.js继续加载其他模块,这些模块中定义的“全局”变量s也互不干扰。所以,node利用javascript的函数式编程的特性,轻而易举地实现了模块的隔离。