开发手册 欢迎您!
软件开发者资料库

模块(Modules) | Node.js

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。Node.js 的包管理器 npm,是全球最大的开源库生态系统。

Node.js v8.x 中文文档


module (模块)#

稳定性: 2 - 稳定的

在 Node.js 模块系统中,每个文件都被视为独立的模块。

例子,假设有一个名为 foo.js 的文件:

const circle = require('./circle.js');console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`);

在第一行中,foo.js 加载了同一目录下的 circle.js 模块。

circle.js 文件的内容为:

const { PI } = Math;exports.area = (r) => PI * r ** 2;exports.circumference = (r) => 2 * PI * r;

circle.js 模块导出了 area()circumference() 两个函数。通过在特殊的 exports 对象上指定额外的属性,函数和对象可以被添加到模块的根部。

模块内的本地变量是私有的,因为模块被 Node.js 包装在一个函数中(详见模块包装器)。在这个例子中,变量 PIcircle.js 私有的。

module.exports属性可以被赋予一个新的值(例如函数或对象)。

如下,bar.js 会用到 square 模块,square 模块导出了 Square 类:

const Square  = require('./square.js');const mySquare = new Square(2);console.log(`mySquare 的面积是 ${mySquare.area()}`);

square 模块定义在 square.js 中:

// 赋值给 `exports` 不会修改模块,必须使用 `module.exports`module.exports = class Square {  constructor(width) {    this.width = width;  }  area() {    return this.width ** 2;  }};模块系统在 `require('module')` 模块中实现。

访问主模块#

当 Node.js 直接运行一个文件时,require.main 会被设为它的 module。这意味着可以通过 require.main === module 来判断一个文件是否被直接运行:

对于 foo.js 文件,如果通过 node foo.js 运行则为 true,但如果通过 require('./foo') 运行则为 false

因为 module 提供了一个 filename 属性(通常等同于 __filename),所以可以通过检查 require.main.filename 来获取当前应用程序的入口点。

附录:包管理器的技巧#

Node.js 的 require() 函数的语义被设计得足够通用化,可以支持许多合理的目录结构。包管理器程序(如 dpkgrpmnpm)可以不用修改就能够从 Node.js 模块构建本地包。

以下是一个推荐的目录结构:

假设想要在 /usr/lib/node// 目录中保存一个特定版本的包的内容。

包可以依赖于其他包。为了安装包 foo,可能需要安装一个指定版本的 bar 包。bar 包也可能有依赖,且在某些情况下,依赖可能有冲突或形成循环。

因为 Node.js 会查找它所加载的模块的实际路径(也就是说会解析符号链接),然后在 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// 目录中。这样 Node.js 就不会在 /usr/node_modules/node_modules 目录中查找缺失的依赖。

为了使模块在 Node.js 的 REPL 中可用,可能需要将 /usr/lib/node_modules 目录添加到 $NODE_PATH 环境变量中。由于在 node_modules 目录中查找模块使用的是相对路径,而调用 require() 的文件是基于实际路径的,因此包本身可以放在任何地方。

总结#

想要获得调用 require() 时加载的确切的文件名,使用 require.resolve() 函数。

综上所述,以下用伪代码描述的高级算法,解释 require.resolve() 做了些什么:

从 Y 路径的模块 require(X)1. 如果 X 是一个核心模块,   a. 返回核心模块   b. 结束2. 如果 X 是以 '/' 开头   a. 设 Y 为文件系统根目录3. 如果 X 是以 './' 或 '/' 或 '../' 开头   a. 加载文件(Y + X)   b. 加载目录(Y + X)4. 加载Node模块(X, dirname(Y))5. 抛出 "未找到"加载文件(X)1. 如果 X 是一个文件,加载 X 作为 JavaScript 文本。结束2. 如果 X.js 是一个文件,加载 X.js 作为 JavaScript 文本。结束3. 如果 X.json 是一个文件,解析 X.json 成一个 JavaScript 对象。结束4. 如果 X.node 是一个文件,加载 X.node 作为二进制插件。结束加载索引(X)1. 如果 X/index.js 是一个文件,加载 X/index.js 作为 JavaScript 文本。结束3. 如果 X/index.json  是一个文件,解析 X/index.json 成一个 JavaScript 对象。结束4. 如果 X/index.node 是一个文件,加载 X/index.node 作为二进制插件。结束加载目录(X)1. 如果 X/package.json 是一个文件,   a. 解析 X/package.json,查找 "main" 字段   b. let M = X + (json main 字段)   c. 加载文件(M)   d. 加载索引(M)2. 加载索引(X)加载Node模块(X, START)1. let DIRS=NODE_MODULES_PATHS(START)2. for each DIR in DIRS:   a. 加载文件(DIR/X)   b. 加载目录(DIR/X)NODE_MODULES_PATHS(START)1. let PARTS = path split(START)2. let I = count of PARTS - 13. let DIRS = []4. while I >= 0,   a. if PARTS[I] = "node_modules" CONTINUE   b. DIR = path join(PARTS[0 .. I] + "node_modules")   c. DIRS = DIRS + DIR   d. let I = I - 15. return DIRS

缓存#

模块在第一次加载后会被缓存。这也意味着(类似其他缓存机制)如果每次调用 require('foo') 都解析到同一文件,则返回相同的对象。

多次调用 require(foo) 不会导致模块的代码被执行多次。这是一个重要的特性。借助它, 可以返回“部分完成”的对象,从而允许加载依赖的依赖, 即使它们会导致循环依赖。

如果想要多次执行一个模块,可以导出一个函数,然后调用该函数。

模块缓存的注意事项#

模块是基于其解析的文件名进行缓存的。由于调用模块的位置的不同,模块可能被解析成不同的文件名(比如从 node_modules 目录加载),这样就不能保证 require('foo') 总能返回完全相同的对象。

此外,在不区分大小写的文件系统或操作系统中,被解析成不同的文件名可以指向同一文件,但缓存仍然会将它们视为不同的模块,并多次重新加载。例如,require('./foo')require('./FOO') 返回两个不同的对象,而不会管 ./foo./FOO 是否是相同的文件。

核心模块#

Node.js 有些模块会被编译成二进制。这些模块别的地方有更详细的描述。

核心模块定义在 Node.js 源代码的 lib/ 目录下。

require() 总是会优先加载核心模块。例如,require('http') 始终返回内置的 HTTP 模块,即使有同名文件。

循环#

当循环调用 require() 时,一个模块可能在未完成执行时被返回。

例如以下情况:

a.js:

console.log('a 开始');exports.done = false;const b = require('./b.js');console.log('在 a 中,b.done = %j', b.done);exports.done = true;console.log('a 结束');

b.js:

console.log('b 开始');exports.done = false;const a = require('./a.js');console.log('在 b 中,a.done = %j', a.done);exports.done = true;console.log('b 结束');

main.js:

console.log('main 开始');const a = require('./a.js');const b = require('./b.js');console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

main.js 加载 a.js 时,a.js 又加载 b.js。此时,b.js 会尝试去加载 a.js。为了防止无限的循环,会返回一个 a.jsexports 对象的 未完成的副本b.js 模块。然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

main.js 加载这两个模块时,它们都已经完成加载。因此,该程序的输出会是:

$ node main.jsmain 开始a 开始b 开始在 b 中,a.done = falseb 结束在 a 中,b.done = truea 结束在 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 文件。

'./' 为前缀的模块是相对于调用 require() 的文件的。也就是说,circle.js 必须和 foo.js 在同一目录下以便于 require('./circle') 找到它。

当没有以 '/''./''../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录。

如果给定的路径不存在,则 require() 会抛出一个 code 属性为 'MODULE_NOT_FOUND'Error

目录作为模块#

可以把程序和库放到一个单独的目录,然后提供一个单一的入口来指向它。把目录递给 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"main" 入口指定的文件不存在,则无法解析,Node.js 会将模块视为不存在,并抛出默认错误:

Error: Cannot find module 'some-library'

如果目录里没有 package.json 文件,则 Node.js 就会试图加载目录下的 index.jsindex.node 文件。例如,如果上面的例子中没有 package.json 文件,则 require('./some-library') 会试图加载:

  • ./some-library/index.js
  • ./some-library/index.node

node_modules 目录加载#

如果传递给 require() 的模块标识符不是一个核心模块,也没有以 '/''../''./' 开头,则 Node.js 会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。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') 会把 path/to/file 解析成相对于 example-module 的位置。后缀路径同样遵循模块的解析语法。

从全局目录加载#

如果 NODE_PATH 环境变量被设为一个以冒号分割的绝对路径列表,则当在其他地方找不到模块时 Node.js 会搜索这些路径。

注意:在 Windows 系统中,NODE_PATH 是以分号间隔的。

在当前的模块解析算法运行之前,NODE_PATH 最初是创建来支持从不同路径加载模块的。

虽然 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 会使用一个如下的函数包装器将其包装:

(function(exports, require, module, __filename, __dirname) {// 模块的代码实际上在这里});

通过这样做,Node.js 实现了以下几点:

  • 它保持了顶层的变量(用 varconstlet 定义)作用在模块范围内,而不是全局对象。
  • 它有助于提供一些看似全局的但实际上是模块特定的变量,例如:
    • 实现者可以用于从模块中导出值的 moduleexports 对象。
    • 包含模块绝对文件名和目录路径的快捷变量 __filename__dirname

The module scope#

__dirname#

示例:运行位于 /Users/mjr目录下的example.js文件:node example.js

console.log(__dirname);// Prints: /Users/mjrconsole.log(path.dirname(__filename));// Prints: /Users/mjr

__filename#

当前模块的文件名称---解析后的绝对路径。

在主程序中这不一定要跟命令行中使用的名称一致。

参阅 __dirname 以获取当前模块的目录名称。

例如:

/Users/mjr 目录下执行 node example.js

console.log(__filename);// Prints: /Users/mjr/example.jsconsole.log(__dirname);// Prints: /Users/mjr

给定两个模块: ab, 其中 ba 的一个依赖。

文件目录结构如下:

  • /Users/mjr/app/a.js
  • /Users/mjr/app/node_modules/b/b.js

b.js 中对 __filename 的引用将会返回 /Users/mjr/app/node_modules/b/b.jsa.js 中对 __filename 的引用将会返回 /Users/mjr/app/a.js

exports#

这是一个对于 module.exports 的更简短的引用形式。查看关于exports shortcut的章节,详细了解什么时候使用exports、什么时候使用module.exports

module#