Skip to content

模块:node:module API

新增于:v0.3.7

Module 对象

在与Module的实例(通常在CommonJS模块中看到的module变量)交互时,提供通用实用程序方法。通过import 'node:module'require('node:module')访问。

module.builtinModules

[历史]

版本变更
v23.5.0此列表现在还包含仅前缀模块。
v9.3.0, v8.10.0, v6.13.0新增于:v9.3.0, v8.10.0, v6.13.0

Node.js 提供的所有模块名称的列表。可用于验证模块是否由第三方维护。

此上下文中的module模块包装器提供的对象不同。要访问它,请require Module 模块:

js
// module.mjs
// 在 ECMAScript 模块中
import { builtinModules as builtin } from 'node:module'
js
// module.cjs
// 在 CommonJS 模块中
const builtin = require('node:module').builtinModules

module.createRequire(filename)

新增于: v12.2.0

  • filename <string> | <URL> 用于构建 require 函数的文件名。必须是文件 URL 对象、文件 URL 字符串或绝对路径字符串。
  • 返回值: <require> require 函数
js
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)

// sibling-module.js 是一个 CommonJS 模块。
const siblingModule = require('./sibling-module')

module.findPackageJSON(specifier[, base])

新增于: v23.2.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.1 - 活跃开发中

  • specifier <string> | <URL> 要检索其 package.json 的模块的标识符。传递 裸标识符 时,返回包根目录下的 package.json。传递 相对标识符绝对标识符 时,返回最近的父 package.json
  • base <string> | <URL> 包含模块的绝对位置(file: URL 字符串或文件系统路径)。对于 CJS,使用 __filename(不是 __dirname!);对于 ESM,使用 import.meta.url。如果 specifier 是一个 绝对标识符,则不需要传递它。
  • 返回值: <string> | <undefined> 如果找到 package.json,则返回路径。当 startLocation 是一个包时,返回包的根 package.json;当是相对的或未解析的时,返回 startLocation 最近的 package.json
text
/path/to/project
  ├ packages/
    ├ bar/
      ├ bar.js
      └ package.json // name = '@foo/bar'
    └ qux/
      ├ node_modules/
        └ some-package/
          └ package.json // name = 'some-package'
      ├ qux.js
      └ package.json // name = '@foo/qux'
  ├ main.js
  └ package.json // name = '@foo'
js
// /path/to/project/packages/bar/bar.js
import { findPackageJSON } from 'node:module'

findPackageJSON('..', import.meta.url)
// '/path/to/project/package.json'
// 传递绝对标识符也能得到相同的结果:
findPackageJSON(new URL('../', import.meta.url))
findPackageJSON(import.meta.resolve('../'))

findPackageJSON('some-package', import.meta.url)
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// 传递绝对标识符时,如果解析的模块位于具有嵌套 `package.json` 的子文件夹内,则可能会得到不同的结果。
findPackageJSON(import.meta.resolve('some-package'))
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'

findPackageJSON('@foo/qux', import.meta.url)
// '/path/to/project/packages/qux/package.json'
js
// /path/to/project/packages/bar/bar.js
const { findPackageJSON } = require('node:module')
const { pathToFileURL } = require('node:url')
const path = require('node:path')

findPackageJSON('..', __filename)
// '/path/to/project/package.json'
// 传递绝对标识符也能得到相同的结果:
findPackageJSON(pathToFileURL(path.join(__dirname, '..')))

findPackageJSON('some-package', __filename)
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// 传递绝对标识符时,如果解析的模块位于具有嵌套 `package.json` 的子文件夹内,则可能会得到不同的结果。
findPackageJSON(pathToFileURL(require.resolve('some-package')))
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'

findPackageJSON('@foo/qux', __filename)
// '/path/to/project/packages/qux/package.json'

module.isBuiltin(moduleName)

新增于:v18.6.0, v16.17.0

  • moduleName <string> 模块名称
  • 返回值: <boolean> 如果模块是内置模块则返回 true,否则返回 false
js
import { isBuiltin } from 'node:module'
isBuiltin('node:fs') // true
isBuiltin('fs') // true
isBuiltin('wss') // false

module.register(specifier[, parentURL][, options])

[历史]

版本变更
v20.8.0, v18.19.0添加对 WHATWG URL 实例的支持。
v20.6.0, v18.19.0新增于:v20.6.0, v18.19.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.2 - 发布候选版本

  • specifier <string> | <URL> 要注册的自定义挂钩;这应该与传递给 import() 的字符串相同,但如果它是相对路径,则相对于 parentURL 解析。
  • parentURL <string> | <URL> 如果要相对于基准 URL(例如 import.meta.url)解析 specifier,可以在这里传递该 URL。默认值: 'data:'
  • options <Object>
    • parentURL <string> | <URL> 如果要相对于基准 URL(例如 import.meta.url)解析 specifier,可以在这里传递该 URL。如果 parentURL 作为第二个参数提供,则忽略此属性。默认值: 'data:'
    • data <any> 要传递到 initialize 挂钩的任何任意、可克隆的 JavaScript 值。
    • transferList <Object[]> 要传递到 initialize 挂钩的 可传输对象

注册一个导出 挂钩 的模块,这些挂钩自定义 Node.js 模块解析和加载行为。请参阅 自定义挂钩

module.registerHooks(options)

新增于: v23.5.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.1 - 活跃开发中

注册自定义 Node.js 模块解析和加载行为的钩子。参见 自定义钩子

module.stripTypeScriptTypes(code[, options])

新增于:v23.2.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.1 - 主动开发中

  • code <字符串> 要从中去除类型注解的代码。

  • options <对象>

    • mode <字符串> 默认值:'strip'。可能的值为:

    • 'strip' 只去除类型注解,不执行 TypeScript 特性的转换。

    • 'transform' 去除类型注解并将 TypeScript 特性转换为 JavaScript。

    • sourceMap <布尔值> 默认值:false。仅当 mode'transform' 时有效,如果为 true,则会为转换后的代码生成源映射。

    • sourceUrl <字符串> 指定源映射中使用的源 URL。

  • 返回值:<字符串> 去除类型注解后的代码。module.stripTypeScriptTypes() 从 TypeScript 代码中去除类型注解。它可以用于在使用 vm.runInContext()vm.compileFunction() 运行 TypeScript 代码之前,去除其中的类型注解。默认情况下,如果代码包含需要转换的 TypeScript 特性(例如 Enums),则会抛出错误,更多信息请参见 类型去除。当 mode'transform' 时,它还会将 TypeScript 特性转换为 JavaScript,更多信息请参见 转换 TypeScript 特性。当 mode'strip' 时,不会生成源映射,因为位置信息会保留。如果提供 sourceMap,且 mode'strip',则会抛出错误。

警告:由于 TypeScript 解析器的更改,此函数的输出不应被认为在 Node.js 版本之间是稳定的。

js
import { stripTypeScriptTypes } from 'node:module'
const code = 'const a: number = 1;'
const strippedCode = stripTypeScriptTypes(code)
console.log(strippedCode)
// 输出:const a         = 1;
js
const { stripTypeScriptTypes } = require('node:module')
const code = 'const a: number = 1;'
const strippedCode = stripTypeScriptTypes(code)
console.log(strippedCode)
// 输出:const a         = 1;

如果提供了 sourceUrl,它将作为注释追加到输出的末尾:

js
import { stripTypeScriptTypes } from 'node:module'
const code = 'const a: number = 1;'
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' })
console.log(strippedCode)
// 输出:const a         = 1\n\n//# sourceURL=source.ts;
js
const { stripTypeScriptTypes } = require('node:module')
const code = 'const a: number = 1;'
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' })
console.log(strippedCode)
// 输出:const a         = 1\n\n//# sourceURL=source.ts;

mode'transform' 时,代码将被转换为 JavaScript:

js
import { stripTypeScriptTypes } from 'node:module'
const code = `
  namespace MathUtil {
    export const add = (a: number, b: number) => a + b;
  }`
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true })
console.log(strippedCode)
// 输出:
// var MathUtil;
// (function(MathUtil) {
//     MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...
js
const { stripTypeScriptTypes } = require('node:module')
const code = `
  namespace MathUtil {
    export const add = (a: number, b: number) => a + b;
  }`
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true })
console.log(strippedCode)
// 输出:
// var MathUtil;
// (function(MathUtil) {
//     MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...

module.syncBuiltinESMExports()

Added in: v12.12.0

module.syncBuiltinESMExports() 方法更新所有内置 ES 模块 的实时绑定,使其与 CommonJS 模块的导出属性匹配。它不会从 ES 模块 中添加或删除导出的名称。

js
const fs = require('node:fs')
const assert = require('node:assert')
const { syncBuiltinESMExports } = require('node:module')

fs.readFile = newAPI

delete fs.readFileSync

function newAPI() {
  // ...
}

fs.newAPI = newAPI

syncBuiltinESMExports()

import('node:fs').then(esmFS => {
  // 它将现有的 readFile 属性与新值同步
  assert.strictEqual(esmFS.readFile, newAPI)
  // readFileSync 已从已加载的 fs 中删除
  assert.strictEqual('readFileSync' in fs, false)
  // syncBuiltinESMExports() 不会从 esmFS 中删除 readFileSync
  assert.strictEqual('readFileSync' in esmFS, true)
  // syncBuiltinESMExports() 不会添加名称
  assert.strictEqual(esmFS.newAPI, undefined)
})

模块编译缓存

[历史]

版本变更
v22.8.0添加了用于运行时访问的初始 JavaScript API。
v22.1.0在 v22.1.0 中添加

模块编译缓存可以通过使用 module.enableCompileCache() 方法或 NODE_COMPILE_CACHE=dir 环境变量来启用。启用后,每当 Node.js 编译 CommonJS 模块或 ECMAScript 模块时,它都会使用保存在指定目录中的基于磁盘的 V8 代码缓存 来加快编译速度。这可能会减慢模块图的第一次加载速度,但是如果模块的内容没有改变,则随后加载相同的模块图可能会获得显著的加速。

要清理磁盘上生成的编译缓存,只需删除缓存目录即可。下次使用相同的目录进行编译缓存存储时,将重新创建缓存目录。为了避免磁盘被陈旧的缓存填满,建议使用 os.tmpdir() 下的目录。如果通过调用 module.enableCompileCache() 启用编译缓存而没有指定目录,则 Node.js 将使用 NODE_COMPILE_CACHE=dir 环境变量(如果已设置),否则默认为 path.join(os.tmpdir(), 'node-compile-cache')。要找到正在运行的 Node.js 实例使用的编译缓存目录,请使用 module.getCompileCacheDir()

目前,当将编译缓存与 V8 JavaScript 代码覆盖率 一起使用时,V8 收集的覆盖率在从代码缓存反序列化的函数中可能不太精确。建议在运行测试以生成精确的覆盖率时将其关闭。

启用的模块编译缓存可以通过 NODE_DISABLE_COMPILE_CACHE=1 环境变量禁用。当编译缓存导致意外或不希望的行为(例如,测试覆盖率精度降低)时,这将非常有用。

一个 Node.js 版本生成的编译缓存不能被不同版本的 Node.js 重用。如果使用相同的基目录来持久化缓存,则不同 Node.js 版本生成的缓存将被分别存储,因此它们可以共存。

目前,当启用编译缓存并重新加载模块时,代码缓存会立即从已编译的代码生成,但只有在 Node.js 实例即将退出时才会写入磁盘。这可能会发生变化。module.flushCompileCache() 方法可用于确保累积的代码缓存被刷新到磁盘,以防应用程序想要生成其他 Node.js 实例并在父进程退出之前很久就让它们共享缓存。

module.constants.compileCacheStatus

新增于:v22.8.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.1 - 活跃开发中

以下常量作为 module.enableCompileCache() 返回的对象中的 status 字段返回,用于指示启用 模块编译缓存 的尝试结果。

常量描述
ENABLEDNode.js 已成功启用编译缓存。用于存储编译缓存的目录将返回在返回对象的 directory 字段中。
ALREADY_ENABLED编译缓存之前已启用,要么通过之前调用 module.enableCompileCache(),要么通过 NODE_COMPILE_CACHE=dir 环境变量启用。用于存储编译缓存的目录将返回在返回对象的 directory 字段中。
FAILEDNode.js 无法启用编译缓存。这可能是由于缺少对指定目录的使用权限,或各种文件系统错误导致的。错误详情将返回在返回对象的 message 字段中。
DISABLEDNode.js 无法启用编译缓存,因为已设置环境变量 NODE_DISABLE_COMPILE_CACHE=1

module.enableCompileCache([cacheDir])

新增于: v22.8.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.1 - 活跃开发中

  • cacheDir <字符串> | <未定义> 可选路径,用于指定编译缓存的存储/读取目录。
  • 返回值: <对象>
    • status <整数> module.constants.compileCacheStatus 中的一个值。
    • message <字符串> | <未定义> 如果 Node.js 无法启用编译缓存,则包含错误消息。仅当 statusmodule.constants.compileCacheStatus.FAILED 时设置。
    • directory <字符串> | <未定义> 如果启用了编译缓存,则包含编译缓存的存储目录。仅当 statusmodule.constants.compileCacheStatus.ENABLEDmodule.constants.compileCacheStatus.ALREADY_ENABLED 时设置。

启用当前 Node.js 实例中的模块编译缓存

如果未指定 cacheDir,Node.js 将使用以下方式之一:如果设置了环境变量 NODE_COMPILE_CACHE=dir,则使用该变量指定的目录;否则,使用 path.join(os.tmpdir(), 'node-compile-cache')。对于一般用例,建议调用 module.enableCompileCache() 时不指定 cacheDir,以便在必要时可以通过 NODE_COMPILE_CACHE 环境变量覆盖目录。

由于编译缓存应该是一个无需应用程序正常运行即可实现的静默优化,因此此方法的设计在无法启用编译缓存时不会抛出任何异常。相反,它将返回一个包含 message 字段中错误消息的对象,以帮助调试。如果成功启用编译缓存,则返回对象中的 directory 字段将包含编译缓存的存储路径。返回对象中的 status 字段将是 module.constants.compileCacheStatus 值之一,用于指示启用模块编译缓存 的尝试结果。

此方法仅影响当前 Node.js 实例。要在子工作线程中启用它,也需要在子工作线程中调用此方法,或者将 process.env.NODE_COMPILE_CACHE 值设置为编译缓存目录,以便行为可以继承到子工作线程中。可以使用此方法返回的 directory 字段或使用 module.getCompileCacheDir() 获取该目录。

module.flushCompileCache()

新增于: v23.0.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.1 - 活跃开发中

将当前 Node.js 实例中已加载模块累积的模块编译缓存刷新到磁盘。此操作在所有刷新文件系统操作结束后返回,无论它们是否成功。如果出现任何错误,此操作将静默失败,因为编译缓存未命中不应干扰应用程序的实际操作。

module.getCompileCacheDir()

新增于: v22.8.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.1 - 活跃开发中

自定义钩子

[历史]

版本变更
v23.5.0添加对同步和线程内钩子的支持。
v20.6.0, v18.19.0添加 initialize 钩子以替换 globalPreload
v18.6.0, v16.17.0添加对链式加载器的支持。
v16.12.0删除 getFormatgetSourcetransformSourceglobalPreload;添加 load 钩子和 getGlobalPreload 钩子。
v8.8.0在 v8.8.0 中添加

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.2 - 预发布版本 (异步版本) 稳定性: 1.1 - 积极开发中 (同步版本)

目前支持两种类型的模块自定义钩子:

启用

模块解析和加载可以通过以下方式自定义:

钩子可以在应用程序代码运行之前使用 --import--require 标志注册:

bash
node --import ./register-hooks.js ./my-app.js
node --require ./register-hooks.js ./my-app.js
js
// register-hooks.js
// 如果此文件不包含顶层 await,则只能 require() 它。
// 使用 module.register() 在专用线程中注册异步钩子。
import { register } from 'node:module'
register('./hooks.mjs', import.meta.url)
js
// register-hooks.js
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')
// 使用 module.register() 在专用线程中注册异步钩子。
register('./hooks.mjs', pathToFileURL(__filename))
js
// 使用 module.registerHooks() 在主线程中注册同步钩子。
import { registerHooks } from 'node:module'
registerHooks({
  resolve(specifier, context, nextResolve) {
    /* 实现 */
  },
  load(url, context, nextLoad) {
    /* 实现 */
  },
})
js
// 使用 module.registerHooks() 在主线程中注册同步钩子。
const { registerHooks } = require('node:module')
registerHooks({
  resolve(specifier, context, nextResolve) {
    /* 实现 */
  },
  load(url, context, nextLoad) {
    /* 实现 */
  },
})

传递给 --import--require 的文件也可以是依赖项的导出:

bash
node --import some-package/register ./my-app.js
node --require some-package/register ./my-app.js

其中 some-package 具有一个 "exports" 字段,该字段将 /register 导出映射到调用 register() 的文件,例如以下 register-hooks.js 示例。

使用 --import--require 可确保在导入任何应用程序文件(包括应用程序的入口点)之前注册钩子,默认情况下也适用于任何工作线程。

或者,可以从入口点调用 register()registerHooks(),尽管对于应在注册钩子后运行的任何 ESM 代码,必须使用动态 import()

js
import { register } from 'node:module'

register('http-to-https', import.meta.url)

// 由于这是一个动态 `import()`,因此 `http-to-https` 钩子将运行
// 以处理 `./my-app.js` 及其导入或需要的任何其他文件。
await import('./my-app.js')
js
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')

register('http-to-https', pathToFileURL(__filename))

// 由于这是一个动态 `import()`,因此 `http-to-https` 钩子将运行
// 以处理 `./my-app.js` 及其导入或需要的任何其他文件。
import('./my-app.js')

自定义钩子将针对比注册时间晚加载的任何模块及其通过 import 和内置 require 引用的模块运行。用户使用 module.createRequire() 创建的 require 函数只能由同步钩子自定义。

在此示例中,我们正在注册 http-to-https 钩子,但它们仅对随后导入的模块可用——在本例中,是 my-app.js 及其通过 import 或 CommonJS 依赖项中的内置 require 引用的任何内容。

如果 import('./my-app.js') 是静态的 import './my-app.js',则应用程序将在注册 http-to-https 钩子之前就已经加载完成。这是由于 ES 模块规范,其中静态导入首先从树的叶子进行评估,然后返回到树干。my-app.js 中可能存在静态导入,这些导入只有在动态导入 my-app.js 后才会进行评估。

如果使用同步钩子,则支持 importrequire 和使用 createRequire() 创建的用户 require

js
import { registerHooks, createRequire } from 'node:module'

registerHooks({
  /* 同步钩子的实现 */
})

const require = createRequire(import.meta.url)

// 同步钩子会影响 import、require() 和通过 createRequire() 创建的用户 require() 函数。
await import('./my-app.js')
require('./my-app-2.js')
js
const { register, registerHooks } = require('node:module')
const { pathToFileURL } = require('node:url')

registerHooks({
  /* 同步钩子的实现 */
})

const userRequire = createRequire(__filename)

// 同步钩子会影响 import、require() 和通过 createRequire() 创建的用户 require() 函数。
import('./my-app.js')
require('./my-app-2.js')
userRequire('./my-app-3.js')

最后,如果您只想在应用程序运行之前注册钩子,并且不想为此创建一个单独的文件,则可以将 data: URL 传递给 --import

bash
node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("http-to-https", pathToFileURL("./"));' ./my-app.js

链式调用

可以多次调用 register

js
// entrypoint.mjs
import { register } from 'node:module'

register('./foo.mjs', import.meta.url)
register('./bar.mjs', import.meta.url)
await import('./my-app.mjs')
js
// entrypoint.cjs
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')

const parentURL = pathToFileURL(__filename)
register('./foo.mjs', parentURL)
register('./bar.mjs', parentURL)
import('./my-app.mjs')

在这个例子中,注册的钩子将形成链。这些链按照后进先出 (LIFO) 的顺序运行。如果 foo.mjsbar.mjs 都定义了一个 resolve 钩子,它们将按如下顺序调用(注意从右到左):Node.js 默认 ← ./foo.mjs./bar.mjs(从 ./bar.mjs 开始,然后是 ./foo.mjs,最后是 Node.js 默认)。其他所有钩子也是如此。

注册的钩子也会影响 register 本身。在这个例子中,bar.mjs 将通过 foo.mjs 注册的钩子解析和加载(因为 foo 的钩子已经添加到链中)。这允许编写非 JavaScript 语言的钩子,只要先前注册的钩子可以编译成 JavaScript 即可。

register 方法不能从定义钩子的模块内部调用。

registerHooks 的链式调用方式类似。如果混合使用同步和异步钩子,则同步钩子总是在异步钩子开始运行之前运行,也就是说,在最后一个同步钩子运行时,它的下一个钩子包括异步钩子的调用。

js
// entrypoint.mjs
import { registerHooks } from 'node:module'

const hook1 = {
  /* implementation of hooks */
}
const hook2 = {
  /* implementation of hooks */
}
// hook2 run before hook1.
registerHooks(hook1)
registerHooks(hook2)
js
// entrypoint.cjs
const { registerHooks } = require('node:module')

const hook1 = {
  /* implementation of hooks */
}
const hook2 = {
  /* implementation of hooks */
}
// hook2 run before hook1.
registerHooks(hook1)
registerHooks(hook2)

通过模块自定义钩子进行通信

异步钩子在专用线程上运行,与运行应用程序代码的主线程分离。这意味着更改全局变量不会影响其他线程,并且必须使用消息通道在线程之间进行通信。

register 方法可用于将数据传递给 initialize 钩子。传递给钩子的数据可能包括可转移的对象,例如端口。

js
import { register } from 'node:module'
import { MessageChannel } from 'node:worker_threads'

// 此示例演示如何使用消息通道与钩子通信,方法是将 `port2` 发送到钩子。
const { port1, port2 } = new MessageChannel()

port1.on('message', msg => {
  console.log(msg)
})
port1.unref()

register('./my-hooks.mjs', {
  parentURL: import.meta.url,
  data: { number: 1, port: port2 },
  transferList: [port2],
})
js
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')
const { MessageChannel } = require('node:worker_threads')

// 此示例演示如何使用消息通道与钩子通信,方法是将 `port2` 发送到钩子。
const { port1, port2 } = new MessageChannel()

port1.on('message', msg => {
  console.log(msg)
})
port1.unref()

register('./my-hooks.mjs', {
  parentURL: pathToFileURL(__filename),
  data: { number: 1, port: port2 },
  transferList: [port2],
})

同步模块钩子在运行应用程序代码的同一线程上运行。它们可以直接更改主线程访问的上下文中的全局变量。

钩子

module.register() 接受的异步钩子

register 方法可以用来注册一个模块,该模块导出了一组钩子。这些钩子是 Node.js 调用的函数,用于自定义模块解析和加载过程。导出的函数必须具有特定的名称和签名,并且必须作为命名导出导出。

js
export async function initialize({ number, port }) {
  // 接收来自 `register` 的数据。
}

export async function resolve(specifier, context, nextResolve) {
  // 获取 `import` 或 `require` 说明符,并将其解析为 URL。
}

export async function load(url, context, nextLoad) {
  // 获取解析后的 URL 并返回要评估的源代码。
}

异步钩子在一个单独的线程中运行,与运行应用程序代码的主线程隔离。这意味着它是一个不同的领域。主线程可以随时终止钩子线程,因此不要依赖异步操作(例如 console.log)来完成。它们默认继承到子工作进程中。

module.registerHooks() 接受的同步钩子函数

新增于:v23.5.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.1 - 活跃开发中

module.registerHooks() 方法接受同步钩子函数。initialize() 不受支持也不必要,因为钩子实现者可以在调用 module.registerHooks() 之前直接运行初始化代码。

js
function resolve(specifier, context, nextResolve) {
  // 将 `import` 或 `require` 说明符解析为 URL。
}

function load(url, context, nextLoad) {
  // 获取已解析的 URL 并返回要评估的源代码。
}

同步钩子函数在与模块加载相同的线程和相同的 领域 中运行。与异步钩子函数不同,它们默认不会继承到子工作线程中,但是,如果使用由 --import--require 预加载的文件注册钩子函数,则子工作线程可以通过 process.execArgv 继承预加载的脚本。详情请参见 Worker 文档

在同步钩子函数中,用户可以预期 console.log() 的完成方式与他们在模块代码中预期 console.log() 的完成方式相同。

Hook 的约定

Hook 是链的一部分 [/api/module#chaining],即使该链仅由一个自定义(用户提供的)Hook 和始终存在的默认 Hook 组成。Hook 函数嵌套:每个函数都必须始终返回一个普通对象,并且链式调用是通过每个函数调用 next<hookName>() 实现的,它指向后续加载器的 Hook(后进先出顺序)。

返回的值缺少必需属性的 Hook 会触发异常。不调用 next<hookName>() 不返回 shortCircuit: true 的 Hook 也会触发异常。这些错误是为了帮助防止链中意外中断。从 Hook 返回 shortCircuit: true 表示链有意在您的 Hook 处结束。

initialize()

新增于:v20.6.0, v18.19.0

[稳定版:1 - 实验版]

稳定版:1 稳定性:1。2 - 发布候选版本

  • data <任何> 来自 register(loader, import.meta.url, { data }) 的数据。

initialize Hook 仅被 register 接受。registerHooks() 不支持也不需要它,因为同步 Hook 的初始化可以在调用 registerHooks() 之前直接运行。

initialize Hook 提供了一种方法来定义一个自定义函数,该函数在初始化 Hooks 模块时在 Hooks 线程中运行。当通过 register 注册 Hooks 模块时会发生初始化。

此 Hook 可以接收来自 register 调用的数据,包括端口和其他可传输的对象。initialize 的返回值可以是 <Promise>,在这种情况下,它将在主应用程序线程执行恢复之前被等待。

模块自定义代码:

js
// path-to-my-hooks.js

export async function initialize({ number, port }) {
  port.postMessage(`increment: ${number + 1}`)
}

调用方代码:

js
import assert from 'node:assert'
import { register } from 'node:module'
import { MessageChannel } from 'node:worker_threads'

// 此示例展示了如何使用消息通道在主(应用程序)线程和在 Hooks 线程上运行的 Hooks 之间进行通信,方法是将 `port2` 发送到 `initialize` Hook。
const { port1, port2 } = new MessageChannel()

port1.on('message', msg => {
  assert.strictEqual(msg, 'increment: 2')
})
port1.unref()

register('./path-to-my-hooks.js', {
  parentURL: import.meta.url,
  data: { number: 1, port: port2 },
  transferList: [port2],
})
js
const assert = require('node:assert')
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')
const { MessageChannel } = require('node:worker_threads')

// 此示例展示了如何使用消息通道在主(应用程序)线程和在 Hooks 线程上运行的 Hooks 之间进行通信,方法是将 `port2` 发送到 `initialize` Hook。
const { port1, port2 } = new MessageChannel()

port1.on('message', msg => {
  assert.strictEqual(msg, 'increment: 2')
})
port1.unref()

register('./path-to-my-hooks.js', {
  parentURL: pathToFileURL(__filename),
  data: { number: 1, port: port2 },
  transferList: [port2],
})

resolve(specifier, context, nextResolve)

[历史]

版本变更
v23.5.0添加对同步和线程内钩子的支持。
v21.0.0, v20.10.0, v18.19.0属性 context.importAssertionscontext.importAttributes 替换。使用旧名称仍然受支持,但会发出实验性警告。
v18.6.0, v16.17.0添加对链式解析钩子的支持。每个钩子必须调用 nextResolve() 或在其返回值中包含设置为 trueshortCircuit 属性。
v17.1.0, v16.14.0添加对导入断言的支持。

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.2 - 发布候选版本(异步版本)稳定性: 1.1 - 活动开发(同步版本)

  • specifier <字符串>

  • context <对象>

    • conditions <字符串数组> 相关 package.json 的导出条件
    • importAttributes <对象> 一个键值对表示要导入的模块属性的对象
    • parentURL <字符串> | <未定义> 导入此模块的模块,如果这是 Node.js 入口点则为未定义
  • nextResolve <函数> 链中的后续 resolve 钩子,或者在最后一个用户提供的 resolve 钩子之后的 Node.js 默认 resolve 钩子

  • 返回值: <对象> | <Promise> 异步版本采用包含以下属性的对象,或解析为该对象的 Promise。同步版本仅接受同步返回的对象。

    • format <字符串> | <空> | <未定义> 对加载钩子的提示(可能被忽略)'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
    • importAttributes <对象> | <未定义> 用于缓存模块时使用的导入属性(可选;如果排除,则使用输入)
    • shortCircuit <未定义> | <布尔值> 一个信号,表示此钩子打算终止 resolve 钩子链。默认值: false
    • url <字符串> 此输入解析到的绝对 URL

resolve 钩子链负责告诉 Node.js 在哪里查找以及如何缓存给定的 import 语句或表达式,或 require 调用。它可以选择返回一个格式(例如 'module')作为对 load 钩子的提示。如果指定了格式,则 load 钩子最终负责提供最终的 format 值(并且可以自由忽略 resolve 提供的提示);如果 resolve 提供了 format,则即使仅将值传递给 Node.js 默认 load 钩子,也需要自定义 load 钩子。

导入类型属性是将加载的模块保存到内部模块缓存的缓存键的一部分。如果模块应使用与源代码中不同的属性进行缓存,则 resolve 钩子负责返回 importAttributes 对象。

context 中的 conditions 属性是一个条件数组,这些条件将用于匹配此解析请求的包导出条件。它们可用于在其他地方查找条件映射或在调用默认解析逻辑时修改列表。

当前的包导出条件 始终位于传递到钩子的 context.conditions 数组中。为了保证在调用 defaultResolve默认的 Node.js 模块说明符解析行为,传递给它的 context.conditions 数组必须包含最初传递到 resolve 钩子的 context.conditions 数组中的所有元素。

js
// module.register() 接受的异步版本。
export async function resolve(specifier, context, nextResolve) {
  const { parentURL = null } = context

  if (Math.random() > 0.5) {
    // 某些条件。
    // 对于某些或所有说明符,执行一些自定义的解析逻辑。
    // 始终返回 {url: <string>} 格式的对象。
    return {
      shortCircuit: true,
      url: parentURL ? new URL(specifier, parentURL).href : new URL(specifier).href,
    }
  }

  if (Math.random() < 0.5) {
    // 另一个条件。
    // 调用 `defaultResolve` 时,参数可以修改。在这种情况下,它会添加另一个值来匹配条件导出。
    return nextResolve(specifier, {
      ...context,
      conditions: [...context.conditions, 'another-condition'],
    })
  }

  // 推迟到链中的下一个钩子,如果这是最后一个用户指定的加载器,则为 Node.js 默认解析。
  return nextResolve(specifier)
}
js
// module.registerHooks() 接受的同步版本。
function resolve(specifier, context, nextResolve) {
  // 与上面的异步 resolve() 类似,因为它没有任何异步逻辑。
}

load(url, context, nextLoad)

[历史]

版本变更
v23.5.0添加对同步和线程内版本的支持。
v20.6.0添加对格式为 commonjssource 的支持。
v18.6.0, v16.17.0添加对链式加载钩子的支持。每个钩子必须调用 nextLoad() 或在其返回结果中包含设置为 trueshortCircuit 属性。

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.2 - 发布候选版本(异步版本)稳定性: 1.1 - 主动开发(同步版本)

load 钩子提供了一种自定义方法来确定如何解释、检索和解析 URL。它还负责验证导入属性。

format 的最终值必须是以下值之一:

format描述load 返回的 source 的可接受类型
'builtin'加载 Node.js 内置模块不适用
'commonjs'加载 Node.js CommonJS 模块{ 字符串 , ArrayBuffer , TypedArray , null , undefined }
'json'加载 JSON 文件{ 字符串 , ArrayBuffer , TypedArray }
'module'加载 ES 模块{ 字符串 , ArrayBuffer , TypedArray }
'wasm'加载 WebAssembly 模块{ ArrayBuffer , TypedArray }

对于 'builtin' 类型,source 的值会被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。

异步 load 钩子中的警告

使用异步 load 钩子时,为 'commonjs' 提供 source 与不提供 source 的效果截然不同:

  • 提供 source 时,来自此模块的所有 require 调用都将由注册了 resolveload 钩子的 ESM 加载器处理;来自此模块的所有 require.resolve 调用都将由注册了 resolve 钩子的 ESM 加载器处理;只有部分 CommonJS API 可用(例如,没有 require.extensions、没有 require.cache、没有 require.resolve.paths),并且对 CommonJS 模块加载器的猴子补丁将不适用。
  • 如果 source 未定义或为 null,它将由 CommonJS 模块加载器处理,并且 require/require.resolve 调用将不会经过注册的钩子。这种针对空值 source 的行为是暂时的——将来将不支持空值 source

这些警告不适用于同步 load 钩子,在这种情况下,自定义 CommonJS 模块可以使用完整的 CommonJS API 集,并且 require/require.resolve 始终经过注册的钩子。

Node.js 内部异步 load 实现(这是 load 链中最后一个钩子的 next 值)在 format'commonjs' 时返回 sourcenull,以保持向后兼容性。这是一个选择使用非默认行为的示例钩子:

js
import { readFile } from 'node:fs/promises'

// module.register() 接受的异步版本。对于 module.registerSync() 接受的同步版本,不需要此修复。
export async function load(url, context, nextLoad) {
  const result = await nextLoad(url, context)
  if (result.format === 'commonjs') {
    result.source ??= await readFile(new URL(result.responseURL ?? url))
  }
  return result
}

这也同样不适用于同步 load 钩子,在这种情况下,返回的 source 包含由下一个钩子加载的源代码,而不管模块格式如何。

如果基于文本的格式(即 'json''module')的 source 值不是字符串,则使用 util.TextDecoder 将其转换为字符串。

load 钩子提供了一种自定义方法来检索已解析 URL 的源代码。这允许加载器潜在地避免从磁盘读取文件。它还可以用于将无法识别的格式映射到受支持的格式,例如将 yaml 映射到 module

js
// module.register() 接受的异步版本。
export async function load(url, context, nextLoad) {
  const { format } = context

  if (Math.random() > 0.5) {
    // 一些条件
    /*
      对于某些或所有 URL,执行一些自定义逻辑来检索源代码。
      始终返回以下形式的对象:
      {
        format: <字符串>,
        source: <字符串|缓冲区>,
      }.
    */
    return {
      format,
      shortCircuit: true,
      source: '...',
    }
  }

  // 将其委托给链中的下一个钩子。
  return nextLoad(url)
}
js
// module.registerHooks() 接受的同步版本。
function load(url, context, nextLoad) {
  // 与上面的异步 load() 类似,因为它没有任何异步逻辑。
}

在更高级的场景中,这也可以用于将不受支持的源代码转换为受支持的源代码(参见下面的 示例)。

示例

各种模块自定义钩子可以组合使用,以实现对 Node.js 代码加载和评估行为的广泛自定义。

从 HTTPS 导入

下面的钩子注册了钩子以启用对这些标识符的基本支持。虽然这似乎是对 Node.js 核心功能的重大改进,但实际使用这些钩子存在很大的缺点:性能比从磁盘加载文件慢得多,没有缓存,也没有安全性。

js
// https-hooks.mjs
import { get } from 'node:https'

export function load(url, context, nextLoad) {
  // 为了通过网络加载 JavaScript,我们需要获取并返回它。
  if (url.startsWith('https://')) {
    return new Promise((resolve, reject) => {
      get(url, res => {
        let data = ''
        res.setEncoding('utf8')
        res.on('data', chunk => (data += chunk))
        res.on('end', () =>
          resolve({
            // 此示例假设所有网络提供的 JavaScript 都是 ES 模块代码。
            format: 'module',
            shortCircuit: true,
            source: data,
          })
        )
      }).on('error', err => reject(err))
    })
  }

  // 让 Node.js 处理所有其他 URL。
  return nextLoad(url)
}
js
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js'

console.log(VERSION)

使用上述钩子模块,运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs 将根据 main.mjs 中 URL 处的模块打印 CoffeeScript 的当前版本。

转译

Node.js 不理解的格式的源代码可以使用 load 钩子 转换为 JavaScript。

这种方法的性能不如在运行 Node.js 之前转译源文件;转译器钩子应该只用于开发和测试目的。

异步版本
js
// coffeescript-hooks.mjs
import { readFile } from 'node:fs/promises'
import { dirname, extname, resolve as resolvePath } from 'node:path'
import { cwd } from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import coffeescript from 'coffeescript'

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/

export async function load(url, context, nextLoad) {
  if (extensionsRegex.test(url)) {
    // CoffeeScript 文件可以是 CommonJS 或 ES 模块,因此我们希望 Node.js 将任何
    // CoffeeScript 文件与同一位置的 .js 文件的处理方式相同。为了确定 Node.js 如何解释任意 .js
    // 文件,请向上搜索文件系统以查找最近的父 package.json 文件
    // 并读取其“type”字段。
    const format = await getPackageType(url)

    const { source: rawSource } = await nextLoad(url, { ...context, format })
    // 此钩子将所有导入的 CoffeeScript 文件的 CoffeeScript 源代码转换为 JavaScript 源代码。
    const transformedSource = coffeescript.compile(rawSource.toString(), url)

    return {
      format,
      shortCircuit: true,
      source: transformedSource,
    }
  }

  // 让 Node.js 处理所有其他 URL。
  return nextLoad(url)
}

async function getPackageType(url) {
  // `url` 仅在第一次迭代时作为从 load() 钩子传递的解析 url 的文件路径
  // 来自 load() 的实际文件路径将包含文件扩展名,因为这是规范要求的
  // 此简单的真值检查(检查 `url` 是否包含文件扩展名)
  // 适用于大多数项目,但不涵盖某些极端情况(例如
  // 无扩展名文件或以尾随空格结尾的 url)
  const isFilePath = !!extname(url)
  // 如果它是一个文件路径,则获取它所在的目录
  const dir = isFilePath ? dirname(fileURLToPath(url)) : url
  // 组成同一目录中 package.json 的文件路径,
  // 可能存在也可能不存在
  const packagePath = resolvePath(dir, 'package.json')
  // 尝试读取可能不存在的 package.json
  const type = await readFile(packagePath, { encoding: 'utf8' })
    .then(filestring => JSON.parse(filestring).type)
    .catch(err => {
      if (err?.code !== 'ENOENT') console.error(err)
    })
  // 如果 package.json 存在且包含具有值的 `type` 字段,则大功告成
  if (type) return type
  // 否则,(如果不在根目录)继续检查下一个上级目录
  // 如果在根目录,则停止并返回 false
  return dir.length > 1 && getPackageType(resolvePath(dir, '..'))
}
同步版本
js
// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs/promises'
import { registerHooks } from 'node:module'
import { dirname, extname, resolve as resolvePath } from 'node:path'
import { cwd } from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import coffeescript from 'coffeescript'

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/

function load(url, context, nextLoad) {
  if (extensionsRegex.test(url)) {
    const format = getPackageType(url)

    const { source: rawSource } = nextLoad(url, { ...context, format })
    const transformedSource = coffeescript.compile(rawSource.toString(), url)

    return {
      format,
      shortCircuit: true,
      source: transformedSource,
    }
  }

  return nextLoad(url)
}

function getPackageType(url) {
  const isFilePath = !!extname(url)
  const dir = isFilePath ? dirname(fileURLToPath(url)) : url
  const packagePath = resolvePath(dir, 'package.json')

  let type
  try {
    const filestring = readFileSync(packagePath, { encoding: 'utf8' })
    type = JSON.parse(filestring).type
  } catch (err) {
    if (err?.code !== 'ENOENT') console.error(err)
  }
  if (type) return type
  return dir.length > 1 && getPackageType(resolvePath(dir, '..'))
}

registerHooks({ load })

运行钩子

coffee
# main.coffee {#maincoffee}
import { scream } from './scream.coffee'
console.log scream 'hello, world'

import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}"
coffee
# scream.coffee {#screamcoffee}
export scream = (str) -> str.toUpperCase()

使用上述钩子模块,运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffeenode --import ./coffeescript-sync-hooks.mjs ./main.coffee 会导致 main.coffee 在其源代码从磁盘加载后但 Node.js 执行之前被转换为 JavaScript;任何通过任何已加载文件的 import 语句引用的 .coffee.litcoffee.coffee.md 文件也是如此。

导入映射

前面两个例子定义了 load 钩子。这是一个 resolve 钩子的例子。此钩子模块读取一个 import-map.json 文件,该文件定义了要覆盖为其他 URL 的指定符(这是一个对“导入映射”规范的一个小子集的非常简单的实现)。

异步版本
js
// import-map-hooks.js
import fs from 'node:fs/promises'

const { imports } = JSON.parse(await fs.readFile('import-map.json'))

export async function resolve(specifier, context, nextResolve) {
  if (Object.hasOwn(imports, specifier)) {
    return nextResolve(imports[specifier], context)
  }

  return nextResolve(specifier, context)
}
同步版本
js
// import-map-sync-hooks.js
import fs from 'node:fs/promises'
import module from 'node:module'

const { imports } = JSON.parse(fs.readFileSync('import-map.json', 'utf-8'))

function resolve(specifier, context, nextResolve) {
  if (Object.hasOwn(imports, specifier)) {
    return nextResolve(imports[specifier], context)
  }

  return nextResolve(specifier, context)
}

module.registerHooks({ resolve })
使用钩子

使用这些文件:

js
// main.js
import 'a-module'
json
// import-map.json
{
  "imports": {
    "a-module": "./some-module.js"
  }
}
js
// some-module.js
console.log('some module!')

运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.jsnode --import ./import-map-sync-hooks.js main.js 应该打印 some module!

源映射 v3 支持

新增于:v13.7.0, v12.17.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1 - 实验性

用于与源映射缓存交互的辅助函数。当启用源映射解析并且在模块的页脚中找到源映射包含指令时,将填充此缓存。

要启用源映射解析,必须使用标志--enable-source-maps运行 Node.js,或者通过设置NODE_V8_COVERAGE=dir启用代码覆盖率。

js
// module.mjs
// 在 ECMAScript 模块中
import { findSourceMap, SourceMap } from 'node:module'
js
// module.cjs
// 在 CommonJS 模块中
const { findSourceMap, SourceMap } = require('node:module')

module.findSourceMap(path)

新增于:v13.7.0, v12.17.0

path 是应为其获取相应源映射的文件的已解析路径。

类: module.SourceMap

新增于: v13.7.0, v12.17.0

new SourceMap(payload[, { lineLengths }]) {#new-sourcemappayload-{-linelengths-}}

创建一个新的 sourceMap 实例。

payload 是一个对象,其键与 Source map v3 格式 匹配:

lineLengths 是一个可选数组,包含生成的代码中每一行的长度。

sourceMap.payload

用于构建 SourceMap 实例的有效负载的 Getter。

sourceMap.findEntry(lineOffset, columnOffset)

  • lineOffset <数字> 生成的源代码中从零开始的行号偏移量
  • columnOffset <数字> 生成的源代码中从零开始的列号偏移量
  • 返回值: <对象>

给定生成的源文件中的一行偏移量和一列偏移量,如果找到,则返回表示原始文件中 SourceMap 范围的对象;否则返回空对象。

返回的对象包含以下键:

  • generatedLine: <数字> 生成的源代码中范围起点的行偏移量
  • generatedColumn: <数字> 生成的源代码中范围起点的列偏移量
  • originalSource: <字符串> 原始源文件的名称,如 SourceMap 中所报告
  • originalLine: <数字> 原始源代码中范围起点的行偏移量
  • originalColumn: <数字> 原始源代码中范围起点的列偏移量
  • name: <字符串>

返回值表示 SourceMap 中出现的原始范围,基于从零开始的偏移量,不是 错误消息和 CallSite 对象中显示的从 1 开始的行号和列号。

要从 Error 堆栈和 CallSite 对象报告的 lineNumber 和 columnNumber 获取相应的从 1 开始的行号和列号,请使用 sourceMap.findOrigin(lineNumber, columnNumber)

sourceMap.findOrigin(lineNumber, columnNumber)

  • lineNumber <number> 生成的源代码中调用位置的 1-indexed 行号
  • columnNumber <number> 生成的源代码中调用位置的 1-indexed 列号
  • 返回值: <Object>

给定生成的源代码中调用位置的 1-indexed lineNumbercolumnNumber,查找原始源代码中对应的调用位置。

如果提供的 lineNumbercolumnNumber 在任何源映射中都找不到,则返回一个空对象。否则,返回的对象包含以下键:

  • name: <string> | <undefined> 源映射中范围的名称(如果提供)。
  • fileName: <string> 源映射中报告的原始源文件的名称
  • lineNumber: <number> 原始源代码中对应调用位置的 1-indexed lineNumber
  • columnNumber: <number> 原始源代码中对应调用位置的 1-indexed columnNumber