Node.js v16.13.0 文档


目录

CommonJS 模块#

中英对照

稳定性: 2 - 稳定

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

const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${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(`The area of mySquare is ${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 查找它加载的任何模块的 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') 时,它将获得符号链接到 /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 交互式解释器,将 /usr/lib/node_modules 文件夹添加到 $NODE_PATH 环境变量可能会很有用。 由于使用 node_modules 文件夹的模块查找都是相对的,并且基于调用 require() 的文件的真实路径,因此包本身可以位于任何位置。

.mjs 扩展名#

中英对照

无法使用 require() 扩展名为 .mjs 的文件。 尝试这样做会抛出错误.mjs 扩展名是为无法通过 require() 加载的 ECMAScript 模块保留的。 有关更多详细信息,请参阅 ECMAScript 模块

总结#

中英对照

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

综上所述,这里是 require() 的伪代码高级算法:

 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.cache 没有被修改,则多次调用 require('foo') 不会导致模块代码被多次执行。 这是重要的特征。 有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。

要让模块多次执行代码,则导出函数,然后调用该函数。

模块缓存的注意事项#

中英对照

模块根据其解析的文件名进行缓存。 由于模块可能会根据调用模块的位置(从 node_modules 文件夹加载)解析为不同的文件名,因此如果 require('foo') 解析为不同的文件,则不能保证 require('foo') 将始终返回完全相同的对象。

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

核心模块#

中英对照

Node.js 有些模块编译成二进制文件。 这些模块在本文档的其他地方有更详细的描述。

核心模块在 Node.js 源代码中定义,位于 lib/ 文件夹中。

如果将核心模块的标识符传给 require(),则始终优先加载核心模块。 例如,require('http') 将始终返回内置的 HTTP 模块,即使存在该名称的文件。

核心模块也可以使用 node: 前缀来识别,在这种情况下,它会绕过 require 缓存。 例如,require('node:http') 将始终返回内置的 HTTP 模块,即使有该名称的 require.cache 条目。

循环#

中英对照

当有循环 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 尝试加载 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 文件被解释为加载了 process.dlopen() 的编译插件模块。

'/' 为前缀的必需模块是文件的绝对路径。 例如,require('/home/marco/foo.js') 将在 /home/marco/foo.js 加载文件。

'./' 为前缀的必需模块与调用 require() 的文件相关。 也就是说,circle.js 必须和 foo.js 在同一个目录下,require('./circle') 才能找到它。

如果没有前导 '/''./''../' 来指示文件,则该模块必须是核心模块或从 node_modules 文件夹加载。

如果给定的路径不存在,则 require() 将抛出 Error,其 code 属性设置为 'MODULE_NOT_FOUND'

目录作为模块#

中英对照

将程序和库组织到自包含目录中,然后为这些目录提供单个入口点是很方便的。 可以通过三种方式将文件夹作为参数传给 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') 将相对于 example-module 所在的位置解析 path/to/file。 后缀路径遵循相同的模块解析语义。

从全局目录加载#

中英对照

如果 NODE_PATH 环境变量设置为以冒号分隔的绝对路径列表,则 Node.js 将在这些路径中搜索模块(如果它们在其他地方找不到)。

在 Windows 上,NODE_PATH 由分号 (;) 而不是冒号分隔。

在定义当前的模块解析算法之前,NODE_PATH 最初是为了支持从不同路径加载模块而创建的。

NODE_PATH 仍然受支持,但现在 Node.js 生态系统已经确定了用于定位依赖模块的约定,因此不太必要。 有时,当不知道必须设置 NODE_PATH 时,依赖 NODE_PATH 的部署会表现出意外的行为。 有时,模块的依赖项会发生变化,导致在搜索 NODE_PATH 时加载不同的版本(甚至不同的模块)。

此外,Node.js 将在以下 GLOBAL_FOLDERS 列表中搜索:

  • 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

给定两个模块: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.js,而在 a.js 中对 __filename 的引用将返回 /Users/mjr/app/a.js

exports#

中英对照

module.exports 的引用,其输入更短。 有关何时使用 exports 和何时使用 module.exports 的详细信息,请参阅有关导出的快捷方式的章节。

module#

中英对照

对当前模块的引用,请参阅有关 module 对象的部分。 特别是,module.exports 用于定义模块通过 require() 导出和提供的内容。

require(id)#

中英对照

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

用于导入模块、JSON 和本地文件。 模块可以从 node_modules 导入。 可以使用相对路径(例如 ././foo./bar/baz../foo)导入本地模块和 JSON 文件,该路径将根据 __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 将重新加载模块。 这不适用于原生插件,因为重新加载会导致错误。

添加或替换条目也是可能的。 在本地模块之前检查此缓存,如果将匹配本地模块的名称添加到缓存中,则只有 node: 前缀的 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 - 弃用

指导 require 如何处理某些文件扩展名。

将扩展名为 .sjs 的文件处理为 .js

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

已弃用。 过去,此列表用于通过按需编译将非 JavaScript 模块加载到 Node.js 中。 但是,在实践中,有很多更好的方法可以做到这一点,例如通过其他一些 Node.js 程序加载模块,或者提前将它们编译为 JavaScript。

避免使用 require.extensions。 使用可能会导致细微的错误,并且每个注册的扩展程序解决扩展程序的速度都会变慢。

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 字符串引用核心模块,例如 httpfs,则返回包含在解析 requestnull 期间搜索的路径的数组。

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('module "a" is ready');
});

赋值给 module.exports 必须立即完成。 不能在任何回调中完成。 以下不起作用:

x.js:

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

y.js:

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

中英对照

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#

中英对照

  • 类型: <boolean> 如果模块在 Node.js 预加载阶段运行,则为 true

module.loaded#

中英对照

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

module.parent#

中英对照

稳定性: 0 - 弃用: 改为使用 require.mainmodule.children

首先需要这个模块的模块,如果当前模块是当前进程的入口点,则为 null,如果模块是由不是 CommonJS 模块的东西(例如:REPL 或 import)加载的,则为 undefined

module.path#

中英对照

模块的目录名称。 这通常与 module.idpath.dirname() 相同。

module.paths#

中英对照

模块的搜索路径。

module.require(id)#

中英对照

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

为此,必须获得对 module 对象的引用。 由于 require() 返回 module.exports,而 module 通常仅在特定模块的代码中可用,因此必须明确导出才能使用。

Module 对象#

中英对照

本章节移至模块:module 核心模块

Source Map V3 的支持#

中英对照

本章节移至模块:module 核心模块

返回顶部