稳定度: 3 - 锁定
node.js
又一个简单的模块加载系统。在node.js
中,文件和模块是一一对应的。以下例子中,foo.js
加载的同目录下的circle.js
。
foo.js
的内容:
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is '
+ circle.area(4));
circle.js
的内容:
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
circle.js
模块暴露了area()
函数和circumference()
函数。想要为你的模块添加函数或对象,你可以将它们添加至特殊的exports
对象的属性上。
模块的本地变量是私有的,好似模块被包裹在一个函数中。在这个例子中变量PI
是circle.js
私有的。
如果想要你的模块暴露一个函数(例如一个构造函数),或者想要一次赋值就暴露一个完整的对象,而不是一次绑定一个属性,那就将之赋值给module.exports
而不是exports
。
以下,bar.js
使用了暴露了一个构造函数的square
模块:
var square = require('./square.js');
var mySquare = square(2);
console.log('The area of my square is ' + mySquare.area());
square
模块内部:
// assigning to exports will not modify module, must use module.exports
module.exports = function(width) {
return {
area: function() {
return width * width;
}
};
}
模块系统在require("module")
中被实现。
循环依赖
当存在循环的require()
调用。一个模块可能在返回时,被没有被执行完毕。
考虑一下情况:
a.js
:
console.log('a starting');
exports.done = false;
var b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
:
console.log('b starting');
exports.done = false;
var a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
:
console.log('main starting');
var a = require('./a.js');
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
当main.js
加载a.js
,而后a.js
会去加载b.js
。与此同时,b.js
尝试去加载a.js
。为了避免一个无限循环,a.js
会返回一个未完成的副本给b.js
模块。b.js
会接着完成加载,然后它所暴露的值再被提供给a.js
模块。
这样main.js
就完成了它们的加载。因此程序的输出是:
$ iojs main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true
如果在你的程序里有循环依赖,请确保它们按你的计划工作。
核心模块
node.js
中有一些模块是被编译成二进制的。这些模块会在本文档的其他地方详细讨论。
核心模块被定义在node.js
源码的lib/
目录下。
当被require()
时,核心模块总是被优先加载的。例如require('http')
总是会返回内建的HTTP模块,甚至是有一个同名文件时。
文件模块
如果准确的文件名没有被发现,那么node.js
将会依次添加.js
,.json
或.node
后缀名,然后试图去加载。
.js
文件被解释为JavaScript
文本文件,.json
被解释为JSON
文本文件,.node
文件被解释为编译好的插件模块,然后被dlopen
加载。
前缀是'/'
则是文件的绝对路径。例如require('/home/marco/foo.js')
将会加载/home/marco/foo.js
。
前缀是'./'
则是调用require()
的文件的相对路径。也就是说,circle.js
必须与foo.js
在同一目录下,这样require('./circle')
才能找到它。
如果没有'/'
,'./'
或'../'
前缀,模块要么是一个核心模块,或是需要从node_modules
目录中被加载。
如果指定的路径不存在,require()
将会抛出一个code
属性是'MODULE_NOT_FOUND'
的错误。
从node_modules目录中加载
如果传递给require()
的模块标识符不是一个本地模块,也没有以'/'
,'../'
或'./'
开始。那么node.js
将会从当前目录的父目录开始,添加/node_modules
,试图从这个路径来加载模块。
如果还是没有找到模块,那么它会再移至此目录的父目录,如此往复,直至到达文件系统的根目录。
例如,如果一个位于'/home/ry/projects/foo.js'
的文件调用了require('bar.js')
,那么node.js
将会按照以下的路径顺序来查找:
/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
这要求程序本地化(localize)自己的依赖,防止它们崩溃。
你也可以在模块名中加入一个路径后缀,来引用这个模块中特定的一个文件或子模块。例如,require('example-module/path/to/file')
将会从example-module
的位置解析相对路径path/to/file
。路径后缀遵循相同的模块解析语义。
作为模块的目录
在一个单独目录下组织程序和库,然后提供一个单独的入口,是非常便捷的。有三种方法,可以将目录作为require()
的参数,来加载模块。
第一种方法是,在模块的根目录下创建一个package.json
文件,其中指定了main
模块。一个示例package.json
文件:
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
如果这个文件位于./some-library
,那么require('./some-library')
将会试图去加载./some-library/lib/some-library.js
。
这就是node.js
所能够了解package.json
文件的程度。
如果目录中没有package.json
文件,那么node.js
将会视图去加载当前目录中的index.js
或index.node
。例如,如果在上面的例子中没有package.json
,那么require('./some-library')
将会试图加载:
./some-library/index.js
./some-library/index.node
缓存
模块在第一次被加载后,会被缓存。这意味着,如果都解析到了相同的文件,每一次调用require('foo')
都将会返回同一个对象。
多次调用require('foo')
可能不会造成模块代码被执行多次。这是一个重要的特性。有了它,“部分完成”的对象也可以被返回,这样,传递依赖也能被加载,即使它们可能会造成循环依赖。
如果你想要一个模块被多次执行,那么就暴露一个函数,然后执行这个函数。
模块缓存警告
模块的缓存依赖于它们被解析后的文件名。所以调用模块的位置不同,可以会解析出不同的文件名(比如需要从node_modules目录中加载)。所以不能保证require('foo')
总是会返回相同的对象,因为它们可能被解析为了不同的文件。
module对象
- {Object}
每一个模块中,变量module
是一个代表了当前模块的引用。为了方便,module.exports
也可以通过模块作用域中的exports
取得。module
对象实际上不是全局的,而是每个模块本地的。
module.exports
- Object
module.exports
对象是由模块系统创建的。有时这是难以接受的;许多人希望它们的模块是一些类的实例。如果需要这样,那么就将想要暴露的对象赋值给module.exports
。注意,将想要暴露的对象传递给exports
,将仅仅只会重新绑定(rebind)本地变量exports
,所以不要这么做。
例如假设我们正在写一个叫做a.js
的模块:
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
那么在另一个文件中我们可以:
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
主要,对module.exports
的赋值必须立刻完成。它不能在任何的回调函数中完成。以下例子将不能正常工作:
x.js
:
setTimeout(function() {
module.exports = { a: "hello" };
}, 0);
y.js
:
var x = require('./x');
console.log(x.a);
exports快捷方式
exports
变量是一个module.exports
的引用。如果你将一个新的值赋予它,那么它将不再指向先前的那个值。
为了说明这个行为,将require()
的实现假设为这样:
function require(...) {
// ...
function (module, exports) {
// Your module code here
exports = some_func; // re-assigns exports, exports is no longer
// a shortcut, and nothing is exported.
module.exports = some_func; // makes your module export 0
} (module, module.exports);
return module;
}
一个指导方针是,如果你弄不清楚exports
和module.exports
之间的关系,请只使用module.exports
。
module.require(id)
- id String
- Return: 被解析的模块的
module.exports
module.require
方法提供了一种像require()
一样,从源模块中加载模块的方法。
注意,为了这么做,你必须取得module
对象的引用。因为require()
返回module.exports
,并且module
对象是一个典型的只在特定的模块作用域中有效的变量,如果要使用它,必须被明确地导出。
module.id
- String
模块的识别符。通常是被完全解析的文件名。
module.filename
- String
模块完全解析后的文件名。
module.loaded
- Boolean
模块是否加载完成,或者是正在加载的过程中。
module.parent
- Module Object
引用这个模块的模块。
module.children
- Array
这个模块所引入的模块。
总体来说
为了获得require()
被调用时将要被加载的准确文件名,使用require.resolve()
函数。
综上所述,以下是一个require.resolve
所做的事的高级算法伪代码:
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. let M = X + (json main field)
c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text. STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon. STOP
LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_AS_FILE(DIR/X)
b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
c. DIR = path join(PARTS[0 .. I] + "node_modules")
b. DIRS = DIRS + DIR
c. let I = I - 1
5. return DIRS
从全局文件夹加载
如果NODE_PATH
环境变量被设置为了一个以冒号分割的绝对路径列表,那么在找不到模块时,node.js
将会从这些路径中寻找模块(注意:在Windows中,NODE_PATH
是以分号间隔的)。
NODE_PATH
最初被创建,是用来支持在当前的模块解析算法被冻结(frozen)前,从不同的路径加载模块的。
NODE_PATH
仍然被支持,但是,如今node.js
生态圈已经有了放置依赖模块的公约,它已经不那么必要的。有时,当人们没有意识到NODE_PATH
有被设置时,依赖于NODE_PATH
的部署可能会产生出人意料的表现。有时,一个模块的依赖改变了,造成了通过NODE_PATH
,加载了不同版本的模块。
另外,node.js
将会查找以下路径:
- 1: $HOME/.node_modules
- 2: $HOME/.node_libraries
- 3: $PREFIX/lib/node
$HOME
是用户的家目录,$PREFIX
是node.js
中配置的node_prefix
。
由于一些历史原因,高度推荐你将依赖放入node_modules
目录。它会被加载的更快,且可靠性更好。
访问主模块
当一个文件直接由node.js
执行,require.main
将被设置为这个模块。这意味着你可以判断一个文件是否是直接被运行的。
require.main === module
对于一个文件foo.js
,如果通过iojs foo.js
运行,以上将会返回true
。如果通过require('./foo')
,将会返回false
。
因为module
提供了一个filename
属性(通常等于__filename
),所以当前应用的入口点可以通过检查require.main.filename
来获取。
附录:包管理小贴士
node.js
的require()
函数的语义被设计得足够通用,来支持各种目录结构。包管理程序诸如dpkg
,rpm
和npm
将可以通过不修改node.js
模块,来构建本地包。
以下我们给出一个建议的可行的目录结构:
假设/usr/lib/node/<some-package>/<some-version>
中有指定版本包的内容。
包可以依赖于其他包。为了安装foo
包,你可能需要安装特定版本的bar
包。bar
包可能有它自己的依赖,在一些情况下,它们的依赖可以会冲突或者产生循环。
由于node.js
会查找任何它加载的包得真实路径(也就是说,解析symlinks
),解析以下结构的方案非常简单:
- /usr/lib/node/foo/1.2.3/ -
foo
包的内容,1.2.3
版本。 - /usr/lib/node/bar/4.3.2/ -
foo
包所依赖的bar
包的内容。 - /usr/lib/node/foo/1.2.3/node_modules/bar - 指向
/usr/lib/node/bar/4.3.2/
的符号链接。 - /usr/lib/node/bar/4.3.2/node_modules/* - 指向
bar
包所依赖的包的符号链接。
因此,即使有循环依赖,或者依赖冲突,每个模块都能够获取它们使用的特定版本的依赖。
当foo
包中的代码执行require('bar')
,将会获得符号链接/usr/lib/node/foo/1.2.3/node_modules/bar
指向的版本。接着,bar
包种的代码执行require('quux')
,它将会获得符号链接/usr/lib/node/bar/4.3.2/node_modules/quux
指向的版本。
此外,为了优化模块查找的过程,我们将模块放在/usr/lib/node_modules/<name>/<version>
而不是直接放在/usr/lib/node
中。然后在找不到依赖时,node.js
就不会一直去查找/usr/node_modules
或/node_modules
目录了。
为了让模块在node.js
的REPL中可用,可能需要将/usr/lib/node_modules
目录加入到$NODE_PATH
环境变量。因为使用node_modules
目录的模块查找都是使用相对路径,且基于调用require()
的文件的真实路径,因此包本身可以在任何位置。
更多Node.js教程
学习更多Node.js教程