Node.js v16.3.0 文档


目录

CommonJS 模块#

中英对照

稳定性: 2 - 稳定

在 Node.js 模块系统中,每个文件被视为一个独立的模块。 例如,假设一个名为 foo.js 的文件:

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

在第一行中,foo.js 加载与 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 模块:

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/<some-package>/<some-version> 目录中保存一个特定版本的包的内容。

包可以依赖于其他包。 为了安装包 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('quux'),它会获得符号链接 /usr/lib/node/bar/4.3.2/node_modules/quux 指向的版本。

此外,为了进一步优化模块查找过程,不要将包直接放在 /usr/lib/node 目录中,而是将它们放在 /usr/lib/node_modules/<name>/<version> 目录中。 这样 Node.js 就不会在 /usr/node_modules/node_modules 目录中查找缺失的依赖。

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

附录:.mjs 扩展名#

中英对照

不可以 require() 具有 .mjs 扩展名的文件。 试图这样做会抛出错误.mjs 扩展名是保留给 ECMAScript 模块,无法通过 require() 加载。 有关更多详细信息,请参见 ECMAScript 模块

总结#

中英对照

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

综上所述,以下用伪代码描述的高级算法,解释 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 '/'
   a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
   c. THROW "not found"
4. If X begins with '#'
   a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
5. LOAD_PACKAGE_SELF(X, dirname(Y))
6. LOAD_NODE_MODULES(X, dirname(Y))
7. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. 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_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.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. If "main" is a falsy value, GOTO 2.
   c. let M = X + (json main field)
   d. LOAD_AS_FILE(M)
   e. LOAD_INDEX(M)
   f. LOAD_INDEX(X) DEPRECATED
   g. THROW "not found"
2. LOAD_INDEX(X)

LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_PACKAGE_EXPORTS(X, DIR)
   b. LOAD_AS_FILE(DIR/X)
   c. 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 = [GLOBAL_FOLDERS]
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 - 1
5. return DIRS

LOAD_PACKAGE_IMPORTS(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "imports" is null or undefined, return.
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
  ["node", "require"]) defined in the ESM resolver.
5. RESOLVE_ESM_MATCH(MATCH).

LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
   may have a @scope/ prefix and the subpath begins with a slash (`/`).
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
   return.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
   `package.json` "exports", ["node", "require"]) defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)

LOAD_PACKAGE_SELF(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "exports" is null or undefined, return.
4. If the SCOPE/package.json "name" is not the first segment of X, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
   "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
   defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)

RESOLVE_ESM_MATCH(MATCH)
1. let { RESOLVED, EXACT } = MATCH
2. let RESOLVED_PATH = fileURLToPath(RESOLVED)
3. If EXACT is true,
   a. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
      format. STOP
4. Otherwise, if EXACT is false,
   a. LOAD_AS_FILE(RESOLVED_PATH)
   b. LOAD_AS_DIRECTORY(RESOLVED_PATH)
5. THROW "not found"

缓存#

中英对照

模块在第一次加载后会被缓存。 这也意味着(类似其他缓存机制)如果每次调用 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.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true

需要仔细的规划, 以允许循环模块依赖在应用程序内正常工作.

文件模块#

中英对照

如果按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js.json.node 拓展名再加载。

.js 文件会被解析为 JavaScript 文本文件,.json 文件会被解析为 JSON 文本文件。 .node 文件会被解析为通过 process.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 将会试图加载目录下的 index.jsindex.node 文件。 例如,如果前面的例子中没有 package.json 文件,则 require('./some-library') 会试图加载:

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

如果这些尝试失败,则 Node.js 将会使用默认错误报告整个模块的缺失:

Error: Cannot find module 'some-library'

从 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

模块作用域#

__dirname#

中英对照

当前模块的目录名称。 这与 __filenamepath.dirname() 相同。

示例:从 /Users/mjr 运行 node example.js

console.log(__dirname);
// 打印: /Users/mjr
console.log(path.dirname(__filename));
// 打印: /Users/mjr

__filename#

中英对照

当前模块的文件名称。 这是当前的模块文件的符号链接已被解析的绝对路径。

对于一个主程序,这不需要与被用于命令行中的文件名称相同。

查看 __dirname 获取当前模块的目录名称。

示例:

从 /Users/mjr 运行 node example.js

console.log(__filename);
// 打印: /Users/mjr/example.js
console.log(__dirname);
// 打印: /Users/mjr

给定两个模块:a 和 b,其中 ba 的一个依赖,并且有一个如下的目录结构:

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

b.js 中的 __filename 的引用会返回 /Users/mjr/app/node_modules/b/b.js,而 a.js 中的 __filename 的引用会返回 /Users/mjr/app/a.js

exports#

中英对照

module.exports 的一个引用,这样输入更简短。 查看关于 exports 快捷方式的章节,获取何时使用 exports 与何时使用 module.exports 的详细信息。

module#

中英对照

对当前模块的一个引用,查看关于 module 对象的章节。 module.exports 特别地被用于定义一个模块导出什么并且通过 require() 可用。

require(id)#

中英对照

  • id <string> 模块名称或者路径。
  • 返回: <any> 导出的模块内容。

被用于导入模块、JSON、以及本地的文件。 模块可以被从 node_modules 导入。 本地的模块和 JSON 文件可以被使用一个相对的路径(例如 ././foo./bar/baz../foo)导入,这会被根据由 __dirname(如果已定义)指定的目录或者当前工作目录进行解析。 POSIX 风格的相对路径会被以一种与操作系统无关的方式解析,这意味着上面的示例在 Windows 上会以与它们在 Unix 系统上相同的方式工作。

// 使用一个相对于 `__dirname` 或当前工作目录的路径导入一个本地的模块。
// (在 Windows 上,这会解析为 .\path\myLocalModule。)
const myLocalModule = require('./path/myLocalModule');

// 导入一个 JSON 文件:
const jsonData = require('./path/filename.json');

// 导入一个来自 node_modules 的模块或者 Node.js 内置的模块:
const crypto = require('crypto');
require.cache#

中英对照

被引入的模块将被缓存在这个对象中。 从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。 这不适用于原生插件,因为它们的重载将会导致错误。

可以添加或替换入口。 在加载原生模块之前会检查此缓存,如果将与原生模块匹配的名称添加到缓存中,则引入调用将不再获取原生模块。 谨慎使用!

const assert = require('assert');
const realFs = require('fs');

const fakeFs = {};
require.cache.fs = { exports: fakeFs };

assert.strictEqual(require('fs'), fakeFs);
assert.strictEqual(require('node:fs'), realFs);
require.extensions#

稳定性: 0 - 弃用

Instruct require on how to handle certain file extensions.

Process files with the extension .sjs as .js:

require.extensions['.sjs'] = require.extensions['.js'];

Deprecated. In the past, this list has been used to load non-JavaScript modules into Node.js by compiling them on-demand. However, in practice, there are much better ways to do this, such as loading modules via some other Node.js program, or compiling them to JavaScript ahead of time.

Avoid using require.extensions. Use could cause subtle bugs and resolving the extensions gets slower with each registered extension.

require.main#

中英对照

Module 对象,表示当 Node.js 进程启动时加载的入口脚本。 参见访问主模块

entry.js 脚本中:

console.log(require.main);
node entry.js
Module {
  id: '.',
  path: '/absolute/path/to',
  exports: {},
  filename: '/absolute/path/to/entry.js',
  loaded: false,
  children: [],
  paths:
   [ '/absolute/path/to/node_modules',
     '/absolute/path/node_modules',
     '/absolute/node_modules',
     '/node_modules' ] }
require.resolve(request[, options])#

中英对照

  • request <string> 要解析的模块路径。
  • options <Object>
    • paths <string[]> 从中解析模块位置的路径。 如果存在,则这些路径(而不是默认的解析路径)会被使用,但是 GLOBAL_FOLDERS(例如 $HOME/.node_modules)除外,它们始终被包含。 这些路径的每一个被用作模块解析算法的一个起点,这意味着 node_modules 层级被从此位置开始检查。
  • 返回: <string>

使用内部的 require() 机制查找一个模块的位置,但是不会加载该模块,只是返回解析后的文件名。

如果该模块不能被找到,则一个 MODULE_NOT_FOUND 错误被抛出。

require.resolve.paths(request)#

中英对照

返回一个包含 request 解析期间被搜索的路径的数组,或者如果 request 字符串指向一个核心模块(例如 http 或者 fs)则返回 null

module 对象#

中英对照

在每个模块中,module 自由变量是一个对表示当前模块的对象的引用。 为方便起见,module.exports 还可以通过 exports 模块全局地访问 。 module 实际上不是一个全局变量,而是每个模块本地的。

module.children#

中英对照

此模块首次需要的模块对象。

module.exports#

中英对照

module.exports 对象是被 Module 系统创建的。 有时这是不可接受的;许多人希望他们的模块是某个类的一个实例。 为了这样做,则赋值期望的导出对象给 module.exports。 赋值期望的对象给 exports 会简单地重新绑定本地的 exports 变量,这可能不是所期望的。

例如,假设我们正在创建一个名为 a.js 的模块:

const EventEmitter = require('events');

module.exports = new EventEmitter();

// 做一些工作,并且在一段时间之后从模块自身触发 'ready' 事件。
setTimeout(() => {
  module.exports.emit('ready');
}, 1000);

然后,在另一个文件中我们可以做:

const a = require('./a');
a.on('ready', () => {
  console.log('模块 "a" 已就绪');
});

module.exports 的赋值必须被立即地完成。 它不能在任何回调中被完成。 这不会起作用:

x.js:

setTimeout(() => {
  module.exports = { a: 'hello' };
}, 0);

y.js:

const x = require('./x');
console.log(x.a);
exports 快捷方式#

中英对照

exports 变量是在一个模块的文件级作用域内可用的,并且在模块被执行之前被赋值 module.exports 的值。

它允许一个快捷方式,因此 module.exports.f = ... 可以被更简洁地写成 exports.f = ...。 但是,就像任何变量一样,如果一个新的值被赋值给 exports,则它不再被绑定到 module.exports

module.exports.hello = true; // 被从模块的 require 导出
exports = { hello: false };  // 不被导出,仅在模块中可用

module.exports 属性被一个新的对象完全地替换时,通常也重新赋值 exports

module.exports = exports = function Constructor() {
  // ... 等
};

为了说明这种行为,想象 require() 假设的实现,这与 require() 实际完成的非常相似:

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // 模块代码在这。在此示例中,定义一个函数。
    function someFunc() {}
    exports = someFunc;
    // 此时,exports 不再是 module.exports 的一个快捷方式,
    // 并且此模块仍然会导出一个空的默认对象。
    module.exports = someFunc;
    // 此时,该模块现在会导出 someFunc,而不是默认的对象。
  })(module, module.exports);
  return module.exports;
}

module.filename#

中英对照

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

module.id#

中英对照

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

module.isPreloading#

  • Type: <boolean> true if the module is running during the Node.js preload phase.

module.loaded#

中英对照

模块是否已经加载完成,或正在加载中。

module.parent#

稳定性: 0 - 弃用: 改为使用 require.main and module.children

The module that first required this one, or null if the current module is the entry point of the current process, or undefined if the module was loaded by something that is not a CommonJS module (E.G.: REPL or import).

module.path#

The directory name of the module. This is usually the same as the path.dirname() of the module.id.

module.paths#

中英对照

模块的搜索路径。

module.require(id)#

中英对照

module.require() 方法提供了一种加载模块的方法,就像从原始模块调用 require() 一样。

为了做到这个,需要获得一个 module 对象的引用。 因为 require() 会返回 module.exports,且 module 通常只在一个特定的模块代码中有效,所以为了使用它,必须显式地导出。

Module 对象#

中英对照

本章节移至 module 模块

Source Map V3 的支持#

This section was moved to Modules: module core module.