知识共享许可协议
本作品采用知识共享署名-非商业性使用 3.0 未本地化版本许可协议进行许可。

Node.js v4.2.4 手册 & 文档


Modules#

稳定度: 5 - 已锁定

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这个特殊对象下即可。

注意,exportsmodule.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 copyb.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#

  • {Object}

在每一个模块中,变量 module 是一个代表当前模块的对象的引用。 特别地,module.exports 可以通过全局模块对象 exports 获取到。 module 不是事实上的全局对象,而更像是每个模块内部的。

module.exports#

  • {Object}

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#

  • {String}

用于区别模块的标识符。通常是完全解析后的文件名。

module.filename#

  • {String}

模块完全解析后的文件名。

module.loaded#

  • {Boolean}

不论该模块是否加载完毕,或者正在加载的过程中。

module.parent#

  • {Module Object}

引入这个模块的模块。

module.children#

  • {Array}

这个模块引入的所有模块对象。

总体来说...#

为了获取调用 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() 的文件所在真实路径,因此包本身可以放在任何位置。