Modules
Node.js 内置了模块加载系统,文件和模块具有一一对应的关系。举例来说,foo.js 文件加载了同一目录下的 circle.js 模块,则在 foo.js 中可以向如下代码所示加载外部模块:
const circle = require('./circle.js');
console.log( `The area of a circle of radius 4 is ${circle.area(4)}`);
circle.js 的内容:
const PI = Math.PI;
exports.area = (r) => PI * r * r;
exports.circumference = (r) => 2 * PI * r;
circle.js 模块向外暴漏了两个方法:area() 和 circuference()。如果要将对象或函数置于模块的顶层作用域,则可以将它们挂载在 exports 对象下。
模块的本地变量和内部封装的方法都是私有的,在上面的代码中变量 PI 就是 circle.js 私有的变量。
如果你只想从模块输出一个包含一切的函数或对象,那么可以使用 module.exports 而不是 exports。
在下面的代码中,bar.js 引用了 square 模块,该模块输出了一个构造函数:
const square = require('./square.js');
var mySquare = <a href="http://man7.org/linux/man-pages/man2/square.2.html">square(2)</a>;
console.log(`The area of my square is ${mySquare.area()}`);
square 模块定义在 square.js 中:
// assigning to exports will not modify module, must use module.exports
module.exports = (width) => {
return {
area: () => width * width
};
}
require('module') 定义了 Node.js 的模块系统。
访问主模块
由 Node.js 直接访问的入口文件也被叫做主模块,此时 require.main 就等于该模块。这也即是说,你可以通过以下代码测试当前文件是否是主文件:
require.main === module
假设有一个 foo.js 文件,如果运行 node 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 首先会查找模块的 realpath(即遇到软链接会解析为真是链接),然后查找存储依赖的 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') 请求加载 bar 后,系统会解析 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 的 REPL 中使用模块,最好将 /usr/lib/node_modules 添加给换进变量 $NODE_PATH。因为模块查找的 node_modules 文件夹使用的都是相对路径,且调用基于文件的真实路径执行 require(),所以包实际上可以置于任意位置。
通过 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
缓存
系统第一次加载模块时会缓存这些模块,这也就是说,每次调用 require('foo') 都会得到相同的返回对象。
缓存有一个很重要的特性的,那就是多次调用 require('foo') 并不会让该模块多次运行。根据该特性,当模块返回结束对象之后,即使其他依赖存在循环引用也无所谓。
如果你想多次调用模块的某块代码,最好的方法是从该模块向外输出一个函数,在外部多次调用该函数。
模块缓存警告
缓存是基于模块的文件名进行解析的,所以如果调用位置不同,加载的模块就有可能不同,也就是说在不同的文件中,无法保证 require('foo') 每次都返回相同的对象。
此外,在对大小写敏感的操作系统中,不同的文件名有可能指向相同的文件,但缓存仍将其视为不同的模块,继而会多次重载该模块。比如,require('./foo') 和 require('./FOO') 会返回两个不同的对象,系统并不会检查 ./foo 和 ./FOO 是否会指向同一个文件。
核心模块
Node.js 内置了一些已经编译成二进制文件的模块,有关这些模块的详细介绍请参考本文的相应章节。
核心模块由 Node.js 源代码定义和实现,保存在 lib/ 文件夹中。
require() 函数总是优先加载核心模块,举例来说,即使存在 http 文件,require('http') 也总会返回一个 Node.js 内建的 HTTP 模块。
循环引用
当 require() 出现循环引用时,引用的模块内部可能尚未执行完就返回了值。
我们假设有三个文件,其中 a.js:
console.log('a starting');
exports.done = false;
const 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;
const 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');
const a = require('./a.js');
const 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 时,发现 b.js 有引用了 a.js。为了避免无限循环引用,系统就会将一个 a.js 的不完全拷贝返回给 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.js 就会尝试使用不同的扩展名去加载模块,比如 .js、.json,最后是 .node。
.js 文件会被解析为 JavaScript 文本文件,.json 会被解析为 JSON 文本文件,.node 文件会被解析为被 dlopen 解析过的插件模块。
以 / 开头的路径为文件的绝对路径,举例来说,对于 require('/home/marco/foo.js'),系统会查找 /home/marco/foo.js。
以 ./ 开头的路径为文件的相对路径,表示相对于当前文件所在的目录,举例来说,对于 foo.js 文件中的 require('./circle'),系统会在 foo.js 所在目录下查找 cicle.js。
对于不以 /、./ 或 ../ 开头的路径,系统会从核心模块或 node_modules 目录查找模块。
如果指定的路径不存在,require() 会抛出一个 Error 实例,该实例具有一个值为 MODULE_NOT_FOUND 的 code 属性。
文件夹即模块
将程序和依赖打包到同一个文件夹内并提供一个入口文件,是一种非常便捷的打包方式。这里有三种方式使用 require() 加载此类模块。
第一种方式是在根目录创建一个 package.json 文件,用于指定 main 模块:
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
如果该模块位于 ./some-library,那么 require('./some-libaray') 就会尝试加载 ./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
从 node_module 加载模块
如果传给 require() 的不是一个原生模块,且不以 /、./ 或 ../ 开头,那么 Node.js 就会从当前模块的父级目录查找 node_modules,并尝试从 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
这种方式有助于限制依赖的作用范围,避免冲突。
开发者还可以通过添加后缀加载指定的文件或子模块,举例来说,require('example-module/path/to/file') 会加载 example-module 模块下的 path/to/file 文件。模块后缀的路径解析方式和上述模块路径的惊喜方式一致。
从全局文件夹加载
Node.js 如果在上述所有地方都找不到模块的话,就会检索环境变量 NODE_PATH 下是否存在,在大多数的系统中,NODE_PATH 中的路径以冒号分隔,而在 Windows 中,NODE_PATH 以逗号分隔。
创建环境变量 NODE_PATH 的本意是在上述的模块检索算法失效后检索更多的 Node.js 路径。
虽然现在 Node.js 还支持环境变量 NODE_PATH,但该变量已经越来越不重要了,这是因为 Node.js 圈约定俗成的使用本地存放依赖模块。如果模块和 NODE_PATH 绑定的话,会让那些不知道 NODE_PATH 的用户茫然不知所措。此外,当模块的依赖关系发生变化时,那么检索 NODE_PATH 就会加载不同版本的模块,甚至是与预期完全不同的模块。
除了 NODE_PATH,Node.js 还会检索以下位置:
$HOME/.node_modules$HOME/.node_libraries$PREFIX/lib/node
这里的 $HOME 是用户的主页目录,$PREFIX 是 Node.js 配置的 node_prefix。
上述处理方式大都是由于历史原因形成的。强烈建议开发者将依赖存储于 node_modules 文件夹,这将有助于提高系统的解析速度,增强模块的稳定性。
module 对象
- 对象
在每一个模块中,变量 module 就是一个引用当前模块的对象。为了简便起见,可以使用 exports 替代 module.exports。module 实际上并不是一个全局对象,而是存在于每一个模块的本地变量。
module.children
- 数组
该属性的值包含了当前模块所加载的 module 对象。
module.exports
- 对象
module.exports 对象由模块系统所创建。很多开发者想要让模块是类的实例,那么就可以将期望的对象赋值给 module.exports。注意,如果赋值给了 exports,实际上只是简单地重新绑定到了本地的 exports 变量,这有可能会发生意料之外的结果。
举例来说,假设我们有一个模块 a.js:
const EventEmitter = require('events');
module.exports = new EventEmitter();
// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(() => {
module.exports.emit('ready');
}, 1000);
在其他文件中加载该模块:
const a = require('./a');
a.on('ready', () => {
console.log('module a is ready');
});
注意,module.exports 的赋值语句必须立即执行,而不能将其置于任何的回调函数之中。在下面的情况下,module.exports 的赋值操作是无效的:
// x.js
setTimeout(() => {
module.exports = { a: 'hello' };
}, 0);
加载 x.js:
const x = require('./x');
console.log(x.a);
exports 和 module.exports
模块中的变量 exports 最开始的时候是对 module.exports 的引用。如果开发者给 exports 替换成了对其他变量的引用,那么两者之间就没有任何关系了:
function require(...) {
// ...
((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 之间的关系,建议你忽略 exports,一切都使用 module.exports。
module.filename
- 字符串
该属性表示模块解析后的文件名。
module.id
- 字符串
该属性表示模块的标识符,通产来说就是模块解析后的文件名。
module.loaded
- 布尔值
该属性表示模块是已经加载完成还是处于加载过程中。
module.parent
- 对象,模块对象
该属性表示第一个加载该模块的文件。
module.require(id)
id,字符串- 返回值类型:对象,模块解析后返回的
module.exports
module.require() 方法提供了一种类似原始模块调用 require() 的模块加载方式。
注意,开发者必须获得 module 对象的引用才可以这么做。因为 require() 需要返回 module.exports,而 module 只代表特定的模块,所以必须显式导出 module 才能使用它。