Node.js v18.18.2 文档


目录

模块:CommonJS 模块#

稳定性:2 - 稳定

CommonJS 模块是为 Node.js 打包 JavaScript 代码的原始方式。Node.js 还支持浏览器和其他 JavaScript 运行时使用的ECMAScript 模块标准。

在 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中定义:

// Assigning to exports will not modify module, must use module.exports
module.exports = class Square {
  constructor(width) {
    this.width = width;
  }

  area() {
    return this.width ** 2;
  }
}; 

CommonJS 模块系统在module核心模块中实现。

启用#

Node.js 有两个模块系统:CommonJS 模块和ECMAScript 模块

默认情况下,Node.js 会将以下内容视为 CommonJS 模块:

  • 扩展名为.cjs的文件;

  • 当最近的父package.json文件包含值为"commonjs"的顶级字段 "type" 时,具有 .js扩展名的文件。

  • 当最近的父 package.json 文件不包含顶级字段 "type" 时,文件扩展名为 .js 。包作者应该包含"type"字段,即使在所有源都是 CommonJS 的包中也是如此。明确包的type将使构建工具和加载器更容易确定如何解释包中的文件。

  • 扩展名不是.mjs.cjs.json.node.js的文件 (当最近的父package.json文件包含一个顶级字段 "type"其值为"module",这些文件只有在通过以下方式包含时才会被识别为 CommonJS 模块require(),不用作程序的命令行入口点)。

有关更多详细信息,请参阅确定模块系统。

调用require()始终使用 CommonJS 模块加载器。调用import() 始终使用 ECMAScript 模块加载器。

访问主模块#

当文件直接从 Node.js 运行时,require.main设置为其 module。这意味着可以通过测试require.main === module直接确定文件是否已运行。

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

当入口点不是 CommonJS 模块时,require.mainundefined,并且主模块无法访问。

包管理器提示#

Node.js require()函数的语义被设计得足够通用以支持合理的目录结构。诸如dpkgrpmnpm之类的包管理器程序将有望在不进行修改的情况下从 Node.js 模块构建本机包。

下面我们给出了一个可行的建议目录结构:

假设我们想让 /usr/lib/node/<some-package>/<some-version>处的文件夹保存特定版本包的内容。

包可以相互依赖。为了安装软件包foo,可能需要安装特定版本的软件包barbar包 本身可能具有依赖关系,在某些情况下,这些依赖关系甚至可能发生冲突或形成循环依赖关系。

因为 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_modules/<name>/<version>中,而不是直接将包放入/usr/lib/node中。那么 Node.js 就不会费心寻找/usr/node_modules/node_modules中缺少的依赖项。

为了使模块可用于 Node.js REPL,将/usr/lib/node_modules文件夹添加到$NODE_PATH环境变量中可能会很有用。由于使用node_modules文件夹的模块查找都是相对的,并且基于调用 require()的文件的真实路径,因此包本身可以位于任何位置。

.mjs扩展#

由于require()的同步性质,无法使用它来加载 ECMAScript 模块文件。尝试这样做将引发 ERR_REQUIRE_ESM错误。请改用import()

.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 file system 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 = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. DIRS = DIR + DIRS
   d. let I = I - 1
5. return DIRS + GLOBAL_FOLDERS

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_PATH = fileURLToPath(MATCH)
2. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
   format. STOP
3. THROW "not found"

缓存#

模块在第一次加载后会被缓存。这意味着(除其他外)每次调用require('foo')将返回完全相同的对象(如果它解析为同一个文件)。

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

要让模块多次执行代码,请导出函数并调用该函数。

模块缓存注意事项#

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

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

核心模块#

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

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

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

如果某些核心模块的标识符传递给require() ,则始终会优先加载。例如,require('http')将始终返回内置 HTTP 模块,即使存在同名的文件。无需使用node:前缀即可加载的核心模块列表公开为module.builtinModules

循环依赖#

当存在循环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。加载具有不同扩展名的文件(例如.cjs)时,必须将其全名传递给require(),包括其文件扩展名(例如 require('./file.cjs'))。

.json文件被解析为 JSON 文本文件,.node文件被解释为加载了process.dlopen()的已编译插件模块。使用任何其他扩展名(或根本没有扩展名)的文件将被解析为 JavaScript 文本文件。请参阅确定模块系统部分以了解将使用什么解析目标。

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

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

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

如果给定路径不存在,require()将抛出 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

如果目录中 不存在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' 

在上述所有三种情况下,import('./some-library')调用将导致 ERR_UNSUPPORTED_DIR_IMPORT错误。使用包子路径导出子路径导入可以提供与作为模块的文件夹相同的包含组织优势,并且适用于requireimport

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) {
// Module code actually lives in here
}); 

通过这样做,Node.js 实现了一些目标:

  • 它将顶级变量(使用varconstlet定义)保留在模块范围内,而不是全局对象范围内。
  • 它有助于提供一些实际上特定于模块的全局变量,例如:
    • 实现者可用来从模块导出值的moduleexports对象。
    • 方便变量__filename__dirname,包含模块的绝对文件名和目录路径。

模块范围#

__dirname#

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

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

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

__filename#

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

对于主程序,这不一定与命令行中使用的文件名相同。

有关当前模块的目录名称,请参阅__dirname

例子:

/Users/mjr运行node example.js

console.log(__filename);
// Prints: /Users/mjr/example.js
console.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.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导入。本地模块和 JSON 文件可以使用将被解析的相对路径(例如././foo./bar/baz../foo )导入针对由__dirname (如果已定义)命名的目录或当前工作目录。POSIX 风格的相对路径以独立于操作系统的方式解析,这意味着上面的示例在 Windows 上的工作方式与在 Unix 系统上的工作方式相同。

// Importing a local module with a path relative to the `__dirname` or current
// working directory. (On Windows, this would resolve to .\path\myLocalModule.)
const myLocalModule = require('./path/myLocalModule');

// Importing a JSON file:
const jsonData = require('./path/filename.json');

// Importing a module from node_modules or Node.js built-in module:
const crypto = require('node:crypto'); 
require.cache#

当需要时,模块会缓存在该对象中。通过从此对象中删除键值,下一个require将重新加载模块。这不适用于本机插件,重新加载将导致错误。

添加或替换条目也是可能的。在内置模块之前检查此缓存,如果将与内置模块匹配的名称添加到缓存中,则只有 node:前缀的 require 调用才会接收内置模块。小心使用!

const assert = require('node:assert');
const realFs = require('node: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 进程启动时加载的入口脚本,如果程序的入口点不是 CommonJS 模块,则为 undefined对象。请参阅“访问主模块”

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])#

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

如果找不到模块,则会抛出MODULE_NOT_FOUND错误。

require.resolve.paths(request)#

如果 request 字符串引用核心模块,则返回包含在解析requestnull期间搜索的路径的数组,例如httpfs

module对象#

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

module.children#

此模块第一次所需的模块对象。

module.exports#

module.exports对象由Module系统创建。有时这是不可接受的;许多人希望他们的模块成为某个类的实例。为此,请将所需的导出对象分配给module.exports。将所需的对象分配给exports只会重新绑定本地exports变量,这可能不是我们想要的。

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

const EventEmitter = require('node: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的分配。它不能在任何回调中完成。这不起作用:

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; // Exported from require of module
exports = { hello: false };  // Not exported, only available in the module 

module.exports属性被新对象完全替换时,通常还会重新分配exports

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

为了说明该行为,想象一下require()的假设实现 ,它与require() 的实际执行非常相似:

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // Module code here. In this example, define a function.
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
} 

module.filename#

模块的完全解析文件名。

module.id#

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

module.isPreloading#

  • 如果模块在 Node.js 预加载阶段运行,请输入:<boolean> true

module.loaded#

模块是否已完成加载,或者正在加载过程中。

module.parent#

稳定性:0 - 已弃用:请使用require.mainmodule.children代替。

第一个需要此模块的模块,如果当前模块是当前进程的入口点,则为null;如果该模块是由非 CommonJS 模块加载的,则为 undefined (例如:REPL 或import)。

module.path#

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

module.paths#

模块的搜索路径。

module.require(id)#

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

为此,需要获取对module对象的引用。由于require()返回module.exports,并且module通常 在特定模块的代码中可用,因此必须显式导出它才能使用。

Module对象#

此部分已移至 模块:module core module

Source map v3 支持#

此部分已移至 模块:module core module

NodeJS中文文档为Read dev Docs平台提供托管,中文NodeJS文档均由英文版NodeJS文档翻译,版权属于nodejs.org