Modules
Node有一个简易的模块加载系统。在node中,文件和模块是一一对应的。下面示例是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
这个特殊对象下即可
注意,exports
是module.exports
的一个引用,只是为了用起来方便。当你想输出的是例如构造函数这样的单个项目,那么需要使用module.exports
。
// 正确输出构造函数 module.exports = MyConstructor;
模块内的本地变量是私有的。在这里例子中,PI
这个变量就是circle.js
私有的。
模块系统的实现在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
会返回一个unfinished copy给b.js
。然后b.js
就会停止加载,并将其exports
对象返回给a.js
模块。
这样main.js
就把这两个模块都加载完成了。这段程序的输出如下:
$ node 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中有一些模块是编译成二进制的。这些模块在本文档的其他地方有更详细的描述。
核心模块定义在node源代码的lib/
目录下。
require()
总是会优先加载核心模块。例如,require(‘http‘)
总是返回编译好的HTTP模块,而不管是否有这个名字的文件。
文件模块
如果按文件名没有查找到,那么node会添加 .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会从当前模块的父目录开始,尝试在它的/node_modules
文件夹里加载相应模块。
如果没有找到,那么就再向上移动到父目录,直到到达顶层目录位置。
例如,如果位于‘/home/ry/projects/foo.js‘
的文件调用了require(‘bar.js‘)
,那么node查找的位置依次为
/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
这就要求程序员应尽量把依赖放在就近的位置,以防崩溃。
Folders as Modules
可以把程序和库放到一个单独的文件夹里,并提供单一入口来指向它。有三种方法,使一个文件夹可以作为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
。
This is the extent of Node‘s awareness of package.json files.
如果目录里没有package.json这个文件,那么node就会尝试去加载这个路径下的index.js
或者index.node
。例如,若上面例子中,没有package.json,那么require(‘./some-library‘)
就将尝试加载下面的文件:
./some-library/index.js
./some-library/index.node
Caching
模块在第一次加载后会被缓存。这意味着(类似其他缓存)每次调用require(‘foo‘)
的时候都会返回同一个对象,当然,必须是每次都解析到同一个文件。
多次调用 require(foo)
未必会导致模块中的代码执行多次. 这是一个重要的功能. 借助这个功能, 可以返回部分完成的对象; 这样, 传递依赖也能被加载, 即使它们可能导致循环依赖
如果你希望一个模块多次执行,那么就输出一个函数,然后调用这个函数。
Module Caching Caveats
模块的缓存是依赖于解析后的文件名。由于随着调用的位置不同,可能解析到不同的文件(比如需从node_modules
文件夹加载的情况),所以,如果解析到其他文件时,就不能保证require(‘foo‘)
总是会返回确切的同一对象。
The module
Object
在每一个模块中,变量 module
是一个代表当前模块的对象的引用。 特别地,module.exports
可以通过全局模块对象 exports
获取到。 module
不是事实上的全局对象,而更像是每个模块内部的。
module.exports
module.exports
对象是通过模块系统产生的。有时候这是难以接受的,许多人想让他们的模块是某个类的实例。 因此,将要导出的对象赋值给 module.exports
。例如,假设我们有一个模块称之为 a.js
// 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.expors
赋值必须立即生效, 不能在回调中执行, 这样不能工作的
x.js:
setTimeout(function() { module.exports = { a: "hello" }; }, 0);
y.js:
var x = require(‘./x‘); console.log(x.a);
module.require(id)
id
{String}- Return: {Object} 已解析模块的
module.exports
module.require
方法提供了一种像 require()
一样从最初的模块加载一个模块的方法。
注意,为了这样做,你必须取得一个对 module
对象的引用。 require()
返回 module.exports
,并且 module
是一个典型的只能在特定模块作用域内有效的变量,如果要使用它,就必须明确的导出。
module.id
用于区别模块的标识符。通常是完全解析后的文件名。
module.filename
模块完全解析后的文件名。
module.loaded
- {Boolean}
不论该模块是否加载完毕,或者正在加载的过程中。
module.parent
引入这个模块的模块。
module.children
这个模块引入的所有模块对象。
总体来说...
为了获取调用 require
加载的确切的文件名,使用 require.resolve()
函数。
综上所述,下面用伪代码的高级算法形式表达了 require.resolve 是如何工作的:
NODE_MODULES_PATHS(START) 1. let PARTS = path split(START) 2. let ROOT = index of first instance of "node_modules" in PARTS, or 0 3. let I = count of PARTS - 1 4. let DIRS = [] 5. while I > ROOT, 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 6. return DIRS
从全局文件夹加载
如果 NODE_PATH
环境变量设置为一个以冒号分割的绝对路径的列表, 找不到模块时 node 将会从这些路径中搜索模块。 (注意:在 windows 操作系统上,NODE_PATH
是以分号间隔的)
此外,node 将会搜索以下地址:
- 1:
$HOME/.node_modules
- 2:
$HOME/.node_libraries
- 3:
$PREFIX/lib/node
$HOME
是用户的主目录,$PREFIX
是 node 里配置的 node_prefix
。
这些大多是由于历史原因。强烈建议读者将所依赖的模块放到 node_modules
文件夹里。 这样加载的更快也更可靠。
访问主模块
当 Node 直接运行一个文件时,require.main
就被设置为它的 module
。 也就是说你可以判断一个文件是否是直接被运行的
require.main === module
对于一个foo.js
文件,如果通过node foo.js
运行是true
,但是通过require(‘./foo‘)
运行却是false
。
因为module
提供了一个filename
属性(通常等于__filename
), 所以当前程序的入口点可以通过require.main.filename
来获取。
附录: 包管理技巧
Node 的 require()
函数的语义被设计的足够通用化,以支持各种常规目录结构。 包管理程序如 dpkg,rpm 和 npm 将不用修改就能够从 Node 模块构建本地包。
接下来我们将给你一个可行的目录结构建议:
假设我们希望将一个包的指定版本放在 /usr/lib/node/<some-package>/<some-version>
目录中。
包可以依赖于其他包。为了安装包 foo,可能需要安装包 bar 的一个指定版本。 包 bar 也可能有依赖关系,在某些情况下依赖关系可能发生冲突或者形成循环。
因为 Node 会查找它所加载的模块的真实路径(也就是说会解析符号链接), 然后按照上文描述的方式在 node_modules 目录中寻找依赖关系,这种情形跟以下体系结构非常相像:
- /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(‘queue‘)
,将会获得符号链接/usr/lib/node/bar/4.3.2/node_modules/quux
指向的版本。
此外,为了进一步优化模块搜索过程,不要将包直接放在 /usr/lib/node
目录中,而是将它们放在/usr/lib/node_modules/<name>/<version>
目录中。 这样在依赖的包找不到的情况下,就不会一直寻找/usr/node_modules
目录或 /node_modules
目录了。
为了使模块在 node 的 REPL 中可用,你可能需要将 /usr/lib/node_modules
目录加入到 $NODE_PATH
环境变量中。 由于在 node_modules 目录中搜索模块使用的是相对路径,基于调用 require()
的文件所在真实路径,因此包本身可以放在任何位置。