模块:node:moduleAPI#>

模块:node:moduleAPI#>

定制钩子#>

🌐 Customization Hooks

版本历史

版本变更

v25.4.0, v24.13.1

Synchronous and in-thread hooks are now release candidate.

v23.5.0, v22.15.0

Add support for synchronous and in-thread hooks.

v20.6.0, v18.19.0

Added initialize hook to replace globalPreload.

v18.6.0, v16.17.0

Add support for chaining loaders.

v16.12.0

Removed getFormat, getSource, transformSource, and globalPreload; added load hook and getGlobalPreload hook.

v8.8.0

新增于: v8.8.0

Node.js 当前支持两种类型的模块自定义钩子:

🌐 Node.js currently supports two types of module customization hooks:

module.registerHooks(options):接受同步钩子函数,这些函数会直接在加载模块的线程上运行。

module.register(specifier[, parentURL][, options]):将指定符指向导出异步钩子函数的模块。这些函数在单独的加载器线程上运行。

异步钩子会因为线程间通信而产生额外开销,尤其是在模块图中自定义 CommonJS 模块时。在大多数情况下,建议通过 module.registerHooks() 使用同步钩子以简化操作。

🌐 The asynchronous hooks incur extra overhead from inter-thread communication,

and have several caveats especially

when customizing CommonJS modules in the module graph.

In most cases, it's recommended to use synchronous hooks via module.registerHooks()

for simplicity.

同步自定义钩子#>

🌐 Synchronous customization hooks

稳定性: 1.2 - 发布候选版

同步自定义钩子的注册#>

🌐 Registration of synchronous customization hooks

要注册同步自定义钩子,请使用 module.registerHooks(),它直接内联接收 同步钩子函数。

🌐 To register synchronous customization hooks, use module.registerHooks(), which

takes synchronous hook functions directly in-line.

// register-hooks.js

import { registerHooks } from 'node:module';

registerHooks({

resolve(specifier, context, nextResolve) { /* implementation */ },

load(url, context, nextLoad) { /* implementation */ },

});// register-hooks.js

const { registerHooks } = require('node:module');

registerHooks({

resolve(specifier, context, nextResolve) { /* implementation */ },

load(url, context, nextLoad) { /* implementation */ },

});拷贝

在应用代码运行之前使用标志注册钩子#>

🌐 Registering hooks before application code runs with flags

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

🌐 The hooks can be registered before the application code is run by using the

--import or --require flag:

node --import ./register-hooks.js ./my-app.js

node --require ./register-hooks.js ./my-app.js 拷贝

传递给 --import 或 --require 的说明符也可以来自一个包:

🌐 The specifier passed to --import or --require can also come from a package:

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

node --require some-package/register ./my-app.js 拷贝

当 some-package 有一个 "exports" 字段定义 /register 时,将其导出以映射到调用 registerHooks() 的文件,如上面的 register-hooks.js 示例。

🌐 Where some-package has an "exports" field defining the /register

export to map to a file that calls registerHooks(), like the

register-hooks.js examples above.

使用 --import 或 --require 可确保在加载任何应用代码之前注册钩子,包括应用的入口点,并且默认情况下也适用于任何工作线程。

🌐 Using --import or --require ensures that the hooks are registered before any

application code is loaded, including the entry point of the application and for

any worker threads by default as well.

在应用代码运行之前以编程方式注册钩子#>

🌐 Registering hooks before application code runs programmatically

或者,可以从入口点调用 registerHooks()。

🌐 Alternatively, registerHooks() can be called from the entry point.

如果入口点需要加载其他模块,并且加载过程需要自定义,请在注册钩子后使用 require() 或动态 import() 来加载它们。不要在注册钩子的同一个模块中使用静态 import 语句来加载需要自定义的模块,因为静态 import 语句会在导入模块中的任何代码运行之前被求值,包括对 registerHooks() 的调用,无论静态 import 语句在导入模块中的位置如何。

🌐 If the entry point needs to load other modules and the loading process needs to be

customized, load them using either require() or dynamic import() after the hooks

are registered. Do not use static import statements to load modules that need to be

customized in the same module that registers the hooks, because static import statements

are evaluated before any code in the importer module is run, including the call to

registerHooks(), regardless of where the static import statements appear in the importer

module.

import { registerHooks } from 'node:module';

registerHooks({ /* implementation of synchronous hooks */ });

// If loaded using static import, the hooks would not be applied when loading

// my-app.mjs, because statically imported modules are all executed before its

// importer regardless of where the static import appears.

// import './my-app.mjs';

// my-app.mjs must be loaded dynamically to ensure the hooks are applied.

await import('./my-app.mjs');const { registerHooks } = require('node:module');

registerHooks({ /* implementation of synchronous hooks */ });

import('./my-app.mjs');

// Or, if my-app.mjs does not have top-level await or it's a CommonJS module,

// require() can also be used:

// require('./my-app.mjs');拷贝

在应用代码运行之前使用 data: URL 注册钩子#>

🌐 Registering hooks before application code runs with a data: URL

或者,可以将内联 JavaScript 代码嵌入到 data: URL 中,以便在应用代码运行之前注册钩子。例如,

🌐 Alternatively, inline JavaScript code can be embedded in data: URLs to register

the hooks before the application code runs. For example,

node --import 'data:text/javascript,import {registerHooks} from "node:module"; registerHooks(/* hooks code */);' ./my-app.js 拷贝

钩子和链式调用约定#>

🌐 Convention of hooks and chaining

钩子是链的一部分,即使该链只包含一个自定义(用户提供的)钩子和始终存在的默认钩子。

🌐 Hooks are part of a chain, even if that chain consists of only one

custom (user-provided) hook and the default hook, which is always present.

Hook 函数可以嵌套:每个函数必须始终返回一个普通对象,链式调用的结果是每个函数调用 next(),它是对后续加载器的 hook(按后进先出顺序)的引用。

可以多次调用 registerHooks():

🌐 It's possible to call registerHooks() more than once:

// entrypoint.mjs

import { registerHooks } from 'node:module';

const hook1 = { /* implementation of hooks */ };

const hook2 = { /* implementation of hooks */ };

// hook2 runs before hook1.

registerHooks(hook1);

registerHooks(hook2);// entrypoint.cjs

const { registerHooks } = require('node:module');

const hook1 = { /* implementation of hooks */ };

const hook2 = { /* implementation of hooks */ };

// hook2 runs before hook1.

registerHooks(hook1);

registerHooks(hook2);拷贝

在本例中,已注册的钩子将形成链。这些链按后进先出(LIFO)运行。如果 hook1 和 hook2 都定义了一个 resolve 钩子,它们将按如下方式被调用(注意从右到左,先从 hook2.resolve,然后 hook1.resolve,最后是 Node.js 默认):

🌐 In this example, the registered hooks will form chains. These chains run

last-in, first-out (LIFO). If both hook1 and hook2 define a resolve

hook, they will be called like so (note the right-to-left,

starting with hook2.resolve, then hook1.resolve, then the Node.js default):

Node.js 默认 resolve ← hook1.resolve ← hook2.resolve

🌐 Node.js default resolve ← hook1.resolve ← hook2.resolve

这同样适用于所有其他钩子。

🌐 The same applies to all the other hooks.

返回缺少必需属性的值的钩子会触发异常。一个钩子如果没有调用 next() 并且也没有返回 shortCircuit: true,同样会触发异常。这些错误是为了帮助防止链条意外中断。从钩子返回 shortCircuit: true 表示链条在你的钩子处故意结束。

如果在加载其他钩子模块时应该应用钩子,则其他钩子模块应在钩子注册之后加载。

🌐 If a hook should be applied when loading other hook modules, the other hook

modules should be loaded after the hook is registered.

同步自定义钩子的注销#>

🌐 Deregistration of synchronous customization hooks

registerHooks() 返回的对象有一个 deregister() 方法,可以用来从链中移除钩子。一旦调用 deregister(),在模块解析或加载过程中,钩子将不再被调用。

🌐 The object returned by registerHooks() has a deregister() method that can be

used to remove the hooks from the chain. Once deregister() is called, the hooks

will no longer be invoked during module resolution or loading.

这目前仅适用于通过 registerHooks() 注册的同步钩子,而不适用于通过 module.register() 注册的异步钩子。

🌐 This is currently only available for synchronous hooks registered via registerHooks(), not for asynchronous

hooks registered via module.register().

import { registerHooks } from 'node:module';

const hooks = registerHooks({

resolve(specifier, context, nextResolve) {

console.log('resolve hook called for', specifier);

return nextResolve(specifier, context);

},

load(url, context, nextLoad) {

return nextLoad(url, context);

},

});

// At this point, the hooks are active and will be called for

// any subsequent import() or require() calls.

await import('./my-module.mjs');

// Later, remove the hooks from the chain.

hooks.deregister();

// Subsequent loads will no longer trigger the hooks.

await import('./another-module.mjs');const { registerHooks } = require('node:module');

const hooks = registerHooks({

resolve(specifier, context, nextResolve) {

console.log('resolve hook called for', specifier);

return nextResolve(specifier, context);

},

load(url, context, nextLoad) {

return nextLoad(url, context);

},

});

// At this point, the hooks are active and will be called for

// any subsequent require() calls.

require('./my-module.cjs');

// Later, remove the hooks from the chain.

hooks.deregister();

// Subsequent loads will no longer trigger the hooks.

require('./another-module.cjs');拷贝

module.registerHooks() 接受的钩子函数#>

🌐 Hook functions accepted by module.registerHooks()

新增于: v23.5.0, v22.15.0

module.registerHooks() 方法接受以下同步钩子函数。

🌐 The module.registerHooks() method accepts the following synchronous hook functions.

function resolve(specifier, context, nextResolve) {

// Take an `import` or `require` specifier and resolve it to a URL.

}

function load(url, context, nextLoad) {

// Take a resolved URL and return the source code to be evaluated.

} 拷贝

同步钩子在与模块加载相同的线程和相同的 字段 中运行,钩子函数中的代码可以通过全局变量或其他共享状态将值直接传递给被引用的模块。

🌐 Synchronous hooks are run in the same thread and the same realm where the modules

are loaded, the code in the hook function can pass values to the modules being referenced

directly via global variables or other shared states.

与异步钩子不同,同步钩子默认不会被继承到子工作线程中,尽管如果使用 --import 或 --require 预加载的文件注册钩子,子工作线程可以通过 process.execArgv 继承预加载的脚本。详情请参见 Worker 的文档。

🌐 Unlike the asynchronous hooks, the synchronous hooks are not inherited into child worker

threads by default, though if the hooks are registered using a file preloaded by

--import or --require, child worker threads can inherit the preloaded scripts

via process.execArgv inheritance. See the documentation of Worker for details.

同步 resolve(specifier, context, nextResolve)#>

🌐 Synchronous resolve(specifier, context, nextResolve)

版本历史

版本变更

v23.5.0, v22.15.0

Add support for synchronous and in-thread hooks.

specifier

context

conditions 相关 package.json 的导出条件

importAttributes 一个对象,其键值对表示要导入模块的属性

parentURL | 导入此模块的模块,如果这是 Node.js 的入口点,则为未定义

nextResolve 链中的后续 resolve 钩子,或最后一个用户提供的 resolve 钩子之后的 Node.js 默认 resolve 钩子

specifier

context | 如果省略,将使用默认值。提供时,默认值会与提供的属性合并,但以提供的属性为优先。

返回:

format | | load 钩子的提示(可能会被忽略)。它可以是一个模块格式(例如 'commonjs' 或 'module'),也可以是像 'css' 或 'yaml' 这样的任意值。

importAttributes | 缓存模块时要使用的导入属性(可选;如果省略,将使用输入)

shortCircuit | 一个信号,表示此钩子打算终止 resolve 钩子链。默认值: false

url 此输入解析后的绝对 URL

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

🌐 The resolve hook chain is responsible for telling Node.js where to find and

how to cache a given import statement or expression, or require call. It can

optionally return a format (such as 'module') as a hint to the load hook. If

a format is specified, the load hook is ultimately responsible for providing

the final format value (and it is free to ignore the hint provided by

resolve); if resolve provides a format, a custom load hook is required

even if only to pass the value to the Node.js default load hook.

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

🌐 Import type attributes are part of the cache key for saving loaded modules into

the internal module cache. The resolve hook is responsible for returning an

importAttributes object if the module should be cached with different

attributes than were present in the source code.

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

🌐 The conditions property in context is an array of conditions that will be used

to match package exports conditions for this resolution

request. They can be used for looking up conditional mappings elsewhere or to

modify the list when calling the default resolution logic.

当前的 包导出条件 总是包含在传入 hook 的 context.conditions 数组中。为了在调用 defaultResolve 时保证 默认的 Node.js 模块指定符解析行为,传递给它的 context.conditions 数组 必须 包含最初传入 resolve hook 的 context.conditions 数组的 所有 元素。

🌐 The current package exports conditions are always in

the context.conditions array passed into the hook. To guarantee default

Node.js module specifier resolution behavior when calling defaultResolve, the

context.conditions array passed to it must include all elements of the

context.conditions array originally passed into the resolve hook.

import { registerHooks } from 'node:module';

function resolve(specifier, context, nextResolve) {

// When calling `defaultResolve`, the arguments can be modified. For example,

// to change the specifier or to add applicable export conditions.

if (specifier.includes('foo')) {

specifier = specifier.replace('foo', 'bar');

return nextResolve(specifier, {

...context,

conditions: [...context.conditions, 'another-condition'],

});

}

// The hook can also skip default resolution and provide a custom URL.

if (specifier === 'special-module') {

return {

url: 'file:///path/to/special-module.mjs',

format: 'module',

shortCircuit: true, // This is mandatory if nextResolve() is not called.

};

}

// If no customization is needed, defer to the next hook in the chain which would be the

// Node.js default resolve if this is the last user-specified loader.

return nextResolve(specifier);

}

registerHooks({ resolve }); 拷贝

同步 load(url, context, nextLoad)#>

🌐 Synchronous load(url, context, nextLoad)

版本历史

版本变更

v23.5.0, v22.15.0

Add support for synchronous and in-thread version.

url resolve 链返回的 URL

context

conditions 相关 package.json 的导出条件

format | | resolve 钩子链可选择提供的格式。这可以是任何字符串值作为输入;输入值不需要符合下文描述的可接受返回值列表。

importAttributes

nextLoad 链中的后续 load 钩子,或最后一个用户提供的 load 钩子之后的 Node.js 默认 load 钩子

url

context | 如果省略,将提供默认值。如果提供,将与默认值合并,并优先使用提供的属性。在默认 nextLoad 中,如果 url 指向的模块没有明确的模块类型信息,则 context.format 是必需的。

返回:

format 列出的可接受模块格式之一是 下面。

shortCircuit | 一个信号,表示此钩子打算终止 load 钩子链。默认值: false

source | | 用于评估的 Node.js 源

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

🌐 The load hook provides a way to define a custom method for retrieving the

source code of a resolved URL. This would allow a loader to potentially avoid

reading files from disk. It could also be used to map an unrecognized format to

a supported one, for example yaml to module.

import { registerHooks } from 'node:module';

import { Buffer } from 'node:buffer';

function load(url, context, nextLoad) {

// The hook can skip default loading and provide a custom source code.

if (url === 'special-module') {

return {

source: 'export const special = 42;',

format: 'module',

shortCircuit: true, // This is mandatory if nextLoad() is not called.

};

}

// It's possible to modify the source code loaded by the next - possibly default - step,

// for example, replacing 'foo' with 'bar' in the source code of the module.

const result = nextLoad(url, context);

const source = typeof result.source === 'string' ?

result.source : Buffer.from(result.source).toString('utf8');

return {

source: source.replace(/foo/g, 'bar'),

...result,

};

}

registerHooks({ load }); 拷贝

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

🌐 In a more advanced scenario, this can also be used to transform an unsupported

source to a supported one (see Examples below).

load 返回的最终接受格式#>

🌐 Accepted final formats returned by load

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

🌐 The final value of format must be one of the following:

format描述load 返回的 source 可接受类型'addon'加载 Node.js 插件'builtin'加载 Node.js 内置模块'commonjs-typescript'加载带有 TypeScript 语法的 Node.js CommonJS 模块 | | | | 'commonjs'加载 Node.js CommonJS 模块 | | | | 'json'加载 JSON 文件 | | 'module-typescript'加载带有 TypeScript 语法的 ES 模块 | | 'module'加载 ES 模块 | | 'wasm'加载 WebAssembly 模块 |

source 的值在格式 'builtin' 中被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。

🌐 The value of source is ignored for format 'builtin' because currently it is

not possible to replace the value of a Node.js builtin (core) module.

这些类型都对应于 ECMAScript 中定义的类。

具体的对象是一个

具体的对象是一个

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

🌐 If the source value of a text-based format (i.e., 'json', 'module')

is not a string, it is converted to a string using util.TextDecoder.

异步自定义钩子#>

🌐 Asynchronous customization hooks

稳定性: 1.1 - 处于活跃开发中

异步自定义钩子的注意事项#>

🌐 Caveats of asynchronous customization hooks

异步自定义钩子有许多注意事项,并且不确定它们的问题是否可以解决。建议用户改为通过 module.registerHooks() 使用同步自定义钩子,以避免这些注意事项。

🌐 The asynchronous customization hooks have many caveats and it is uncertain if their

issues can be resolved. Users are encouraged to use the synchronous customization hooks

via module.registerHooks() instead to avoid these caveats.

异步钩子在单独的线程上运行,因此钩子函数不能直接修改正在自定义的模块的全局状态。通常使用消息通道和原子操作在两者之间传递数据或影响控制流。参见 与异步模块自定义钩子通信。

异步钩子不会影响模块图中所有的 require() 调用。

使用 module.createRequire() 创建的自定义 require 函数不受影响。

如果异步 load 钩子没有覆盖通过它的 CommonJS 模块的 source,那么由这些 CommonJS 模块通过内置的 require() 加载的子模块也不会受到异步钩子的影响。

在自定义 CommonJS 模块时,异步钩子需要处理几个注意事项。详见 异步 resolve 钩子 和 异步 load 钩子。

当 CommonJS 模块内部的 require() 调用被异步钩子自定义时,Node.js 可能需要多次加载 CommonJS 模块的源代码,以保持与现有 CommonJS monkey-patching 的兼容性。如果模块代码在多次加载之间发生变化,可能会导致意想不到的行为。

作为副作用,如果同时注册了异步钩子和同步钩子,并且异步钩子选择自定义 CommonJS 模块,则同步钩子可能会对该 CommonJS 模块中的 require() 调用被多次调用。

异步自定义钩子的注册#>

🌐 Registration of asynchronous customization hooks

异步自定义钩子使用 module.register() 注册,该 module.register() 接受指向导出 异步钩子函数 的另一个模块的路径或 URL。

🌐 Asynchronous customization hooks are registered using module.register() which takes

a path or URL to another module that exports the asynchronous hook functions.

类似于 registerHooks(),register() 可以在 --import 或 --require 预加载的模块中被调用,或者直接在入口点内被调用。

🌐 Similar to registerHooks(), register() can be called in a module preloaded by --import or

--require, or called directly within the entry point.

// Use module.register() to register asynchronous hooks in a dedicated thread.

import { register } from 'node:module';

register('./hooks.mjs', import.meta.url);

// If my-app.mjs is loaded statically here as `import './my-app.mjs'`, since ESM

// dependencies are evaluated before the module that imports them,

// it's loaded _before_ the hooks are registered above and won't be affected.

// To ensure the hooks are applied, dynamic import() must be used to load ESM

// after the hooks are registered.

import('./my-app.mjs');const { register } = require('node:module');

const { pathToFileURL } = require('node:url');

// Use module.register() to register asynchronous hooks in a dedicated thread.

register('./hooks.mjs', pathToFileURL(__filename));

import('./my-app.mjs');拷贝

在 hooks.mjs 中:

🌐 In hooks.mjs:

// hooks.mjs

export async function resolve(specifier, context, nextResolve) {

/* implementation */

}

export async function load(url, context, nextLoad) {

/* implementation */

} 拷贝

与同步钩子不同,对于在调用 register() 的文件中加载的这些模块,异步钩子不会运行:

🌐 Unlike synchronous hooks, the asynchronous hooks would not run for these modules loaded in the file

that calls register():

// register-hooks.js

import { register, createRequire } from 'node:module';

register('./hooks.mjs', import.meta.url);

// Asynchronous hooks does not affect modules loaded via custom require()

// functions created by module.createRequire().

const userRequire = createRequire(import.meta.filename);

userRequire('./my-app-2.cjs'); // Hooks won't affect this// register-hooks.js

const { register, createRequire } = require('node:module');

const { pathToFileURL } = require('node:url');

register('./hooks.mjs', pathToFileURL(__filename));

// Asynchronous hooks does not affect modules loaded via built-in require()

// in the module calling `register()`

require('./my-app-2.cjs'); // Hooks won't affect this

// .. or custom require() functions created by module.createRequire().

const userRequire = createRequire(__filename);

userRequire('./my-app-3.cjs'); // Hooks won't affect this拷贝

异步钩子也可以使用带有 --import 标志的 data: URL 来注册:

🌐 Asynchronous hooks can also be registered using a data: URL with the --import flag:

node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("my-instrumentation", pathToFileURL("./"));' ./my-app.js 拷贝

异步自定义钩子的链式调用#>

🌐 Chaining of asynchronous customization hooks

register() 的链式调用工作方式类似于 registerHooks()。如果同步和异步钩子混合使用,同步钩子总是先执行,然后才开始执行异步钩子,也就是说,在最后一个运行的同步钩子中,它的下一个钩子包括异步钩子的调用。

🌐 Chaining of register() work similarly to registerHooks(). If synchronous and asynchronous

hooks are mixed, the synchronous hooks are always run first before the asynchronous

hooks start running, that is, in the last synchronous hook being run, its next

hook includes invocation of the asynchronous hooks.

// entrypoint.mjs

import { register } from 'node:module';

register('./foo.mjs', import.meta.url);

register('./bar.mjs', import.meta.url);

await import('./my-app.mjs');// 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');拷贝

如果 foo.mjs 和 bar.mjs 定义了一个 resolve 钩子,它们将像这样被调用

(注意从右到左,先是 ./bar.mjs,然后是 ./foo.mjs,最后是 Node.js 默认的):

🌐 If foo.mjs and bar.mjs define a resolve hook, they will be called like so

(note the right-to-left, starting with ./bar.mjs, then ./foo.mjs, then the Node.js default):

Node.js 默认 ← ./foo.mjs ← ./bar.mjs

🌐 Node.js default ← ./foo.mjs ← ./bar.mjs

在使用异步钩子时,已注册的钩子也会影响随后的 register 调用,它负责加载钩子模块。在上面的示例中,bar.mjs 将通过 foo.mjs 注册的钩子来解析和加载(因为 foo 的钩子已被添加到链中)。这允许编写非 JavaScript 语言的钩子,只要先前注册的钩子能够转换为 JavaScript。

🌐 When using the asynchronous hooks, the registered hooks also affect subsequent

register calls, which takes care of loading hook modules. In the example above,

bar.mjs will be resolved and loaded via the hooks registered by foo.mjs

(because foo's hooks will have already been added to the chain). This allows

for things like writing hooks in non-JavaScript languages, so long as

earlier registered hooks transpile into JavaScript.

register() 方法不能从运行导出异步钩子的钩子模块或其依赖的线程中调用。

🌐 The register() method cannot be called from the thread running the hook module that

exports the asynchronous hooks or its dependencies.

与异步模块自定义钩子的通信#>

🌐 Communication with asynchronous module customization hooks

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

🌐 Asynchronous hooks run on a dedicated thread, separate from the main

thread that runs application code. This means mutating global variables won't

affect the other thread(s), and message channels must be used to communicate

between the threads.

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

🌐 The register method can be used to pass data to an initialize hook. The

data passed to the hook may include transferable objects like ports.

import { register } from 'node:module';

import { MessageChannel } from 'node:worker_threads';

// This example demonstrates how a message channel can be used to

// communicate with the hooks, by sending `port2` to the hooks.

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],

});const { register } = require('node:module');

const { pathToFileURL } = require('node:url');

const { MessageChannel } = require('node:worker_threads');

// This example showcases how a message channel can be used to

// communicate with the hooks, by sending `port2` to the hooks.

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() 接受异步钩子#>

🌐 Asynchronous hooks accepted by module.register()

版本历史

版本变更

v20.6.0, v18.19.0

Added initialize hook to replace globalPreload.

v18.6.0, v16.17.0

Add support for chaining loaders.

v16.12.0

Removed getFormat, getSource, transformSource, and globalPreload; added load hook and getGlobalPreload hook.

v8.8.0

新增于: v8.8.0

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

🌐 The register method can be used to register a module that exports a set of

hooks. The hooks are functions that are called by Node.js to customize the

module resolution and loading process. The exported functions must have specific

names and signatures, and they must be exported as named exports.

export async function initialize({ number, port }) {

// Receives data from `register`.

}

export async function resolve(specifier, context, nextResolve) {

// Take an `import` or `require` specifier and resolve it to a URL.

}

export async function load(url, context, nextLoad) {

// Take a resolved URL and return the source code to be evaluated.

} 拷贝

异步钩子在一个独立的线程中运行,与执行应用代码的主线程隔离。这意味着它是一个不同的字段。钩子线程可能会随时被主线程终止,因此不要依赖异步操作(如 console.log)的完成。它们默认会继承到子工作线程中。

🌐 Asynchronous hooks are run in a separate thread, isolated from the main thread where

application code runs. That means it is a different realm. The hooks thread

may be terminated by the main thread at any time, so do not depend on

asynchronous operations (like console.log) to complete. They are inherited into

child workers by default.

initialize()#>

新增于: v20.6.0, v18.19.0

data register(loader, import.meta.url, { data }) 的数据。

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

🌐 The initialize hook is only accepted by register. registerHooks() does

not support nor need it since initialization done for synchronous hooks can be run

directly before the call to registerHooks().

initialize 钩子提供了一种方式,可以定义一个自定义函数,当 hooks 模块初始化时,该函数将在 hooks 线程中运行。当通过 register 注册 hooks 模块时,初始化就会发生。

🌐 The initialize hook provides a way to define a custom function that runs in

the hooks thread when the hooks module is initialized. Initialization happens

when the hooks module is registered via register.

这个钩子可以从 register 调用接收数据,包括端口和其他可传递对象。initialize 的返回值可以是一个 ,在这种情况下,它将在主应用线程执行恢复之前被等待。

🌐 This hook can receive data from a register invocation, including

ports and other transferable objects. The return value of initialize can be a

, in which case it will be awaited before the main application thread

execution resumes.

模块定制代码:

🌐 Module customization code:

// path-to-my-hooks.js

export async function initialize({ number, port }) {

port.postMessage(`increment: ${number + 1}`);

} 拷贝

调用者代码:

🌐 Caller code:

import assert from 'node:assert';

import { register } from 'node:module';

import { MessageChannel } from 'node:worker_threads';

// This example showcases how a message channel can be used to communicate

// between the main (application) thread and the hooks running on the hooks

// thread, by sending `port2` to the `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],

});const assert = require('node:assert');

const { register } = require('node:module');

const { pathToFileURL } = require('node:url');

const { MessageChannel } = require('node:worker_threads');

// This example showcases how a message channel can be used to communicate

// between the main (application) thread and the hooks running on the hooks

// thread, by sending `port2` to the `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)#>

🌐 Asynchronous resolve(specifier, context, nextResolve)

版本历史

版本变更

v21.0.0, v20.10.0, v18.19.0

The property context.importAssertions is replaced with context.importAttributes. Using the old name is still supported and will emit an experimental warning.

v18.6.0, v16.17.0

Add support for chaining resolve hooks. Each hook must either call nextResolve() or include a shortCircuit property set to true in its return.

v17.1.0, v16.14.0

Add support for import assertions.

specifier

context

conditions 相关 package.json 的导出条件

importAttributes 一个对象,其键值对表示要导入模块的属性

parentURL | 导入此模块的模块,如果这是 Node.js 的入口点,则为未定义

nextResolve 链中的后续 resolve 钩子,或最后一个用户提供的 resolve 钩子之后的 Node.js 默认 resolve 钩子

specifier

context | 如果省略,将使用默认值。提供时,默认值会与提供的属性合并,但以提供的属性为优先。

返回: | 异步版本接受一个包含以下属性的对象,或者一个将解析为此类对象的 Promise。

format | | load 钩子的提示(可能会被忽略)。它可以是一个模块格式(例如 'commonjs' 或 'module'),也可以是像 'css' 或 'yaml' 这样的任意值。

importAttributes | 缓存模块时要使用的导入属性(可选;如果省略,将使用输入)

shortCircuit | 一个信号,表示此钩子打算终止 resolve 钩子链。默认值: false

url 此输入解析后的绝对 URL

异步版本的工作原理与同步版本类似,只是 nextResolve 函数返回一个 Promise,并且 resolve 钩子本身可以返回一个 Promise。

🌐 The asynchronous version works similarly to the synchronous version, only that the

nextResolve function returns a Promise, and the resolve hook itself can return a Promise.

警告 在异步版本中,尽管支持返回 promise 和异步函数,调用 resolve 仍可能阻塞主线程,从而影响性能。

警告 对 CommonJS 模块中的 require() 调用调用的 resolve 钩子

被异步钩子自定义时,不会接收到传递给 require() 的原始标识符。

相反,它接收到的是已经使用默认 CommonJS 解析完全解析的 URL。

警告 在由异步自定义钩子定制的 CommonJS 模块中,

require.resolve() 和 require() 将使用 "import" 导出条件而不是

"require",这可能在加载双重包时导致意外行为。

export async function resolve(specifier, context, nextResolve) {

// When calling `defaultResolve`, the arguments can be modified. For example,

// to change the specifier or add conditions.

if (specifier.includes('foo')) {

specifier = specifier.replace('foo', 'bar');

return nextResolve(specifier, {

...context,

conditions: [...context.conditions, 'another-condition'],

});

}

// The hook can also skips default resolution and provide a custom URL.

if (specifier === 'special-module') {

return {

url: 'file:///path/to/special-module.mjs',

format: 'module',

shortCircuit: true, // This is mandatory if not calling nextResolve().

};

}

// If no customization is needed, defer to the next hook in the chain which would be the

// Node.js default resolve if this is the last user-specified loader.

return nextResolve(specifier);

} 拷贝

异步 load(url, context, nextLoad)#>

🌐 Asynchronous load(url, context, nextLoad)

版本历史

版本变更

v22.6.0

Add support for source with format commonjs-typescript and module-typescript.

v20.6.0

Add support for source with format commonjs.

v18.6.0, v16.17.0

Add support for chaining load hooks. Each hook must either call nextLoad() or include a shortCircuit property set to true in its return.

url resolve 链返回的 URL

context

conditions 相关 package.json 的导出条件

format | | resolve 钩子链可选择提供的格式。这可以是任何字符串值作为输入;输入值不需要符合下文描述的可接受返回值列表。

importAttributes

nextLoad 链中的后续 load 钩子,或最后一个用户提供的 load 钩子之后的 Node.js 默认 load 钩子

url

context | 如果省略,将提供默认值。如果提供,将与默认值合并,并优先使用提供的属性。在默认 nextLoad 中,如果 url 指向的模块没有明确的模块类型信息,则 context.format 是必需的。

返回: 异步版本接受一个包含以下属性的对象,或者一个将解析为此类对象的 Promise。

format

shortCircuit | 一个信号,表示此钩子打算终止 load 钩子链。默认值: false

source | | 用于评估的 Node.js 源

警告:异步 load 钩子与来自 CommonJS 模块的命名空间导出不兼容。尝试将它们一起使用将导致导入结果为空对象。未来可能会解决此问题。这不适用于同步 load 钩子,在这种情况下可以像平常一样使用导出。

异步版本的工作方式与同步版本类似,但在使用异步 load 钩子时,省略与提供 'commonjs' 的 source 会产生非常不同的效果:

🌐 The asynchronous version works similarly to the synchronous version, though

when using the asynchronous load hook, omitting vs providing a source for

'commonjs' has very different effects:

当提供 source 时,该模块中的所有 require 调用将由 ESM 加载器处理,并使用已注册的 resolve 和 load 钩子;该模块中的所有 require.resolve 调用将由 ESM 加载器处理,并使用已注册的 resolve 钩子;只有部分 CommonJS API 可用(例如没有 require.extensions、没有 require.cache、没有 require.resolve.paths),对 CommonJS 模块加载器的猴子补丁将不生效。

如果 source 是未定义或 null,它将由 CommonJS 模块加载器处理,并且 require/require.resolve 调用不会经过注册的钩子。对于 null 值的 source,这种行为是暂时的——将来将不再支持 null 值的 source。

这些注意事项不适用于同步的 load 钩子,在这种情况下,自定义 CommonJS 模块可以使用完整的 CommonJS API 集,require/require.resolve 始终会通过已注册的钩子。

🌐 These caveats do not apply to the synchronous load hook, in which case

the complete set of CommonJS APIs available to the customized CommonJS

modules, and require/require.resolve always go through the registered

hooks.

Node.js 内部的异步 load 实现,即 load 链中最后一个钩子的 next 值,当 format 为 'commonjs' 时,会为了向后兼容而返回 null 作为 source。下面是一个示例钩子,它将选择使用非默认行为:

🌐 The Node.js internal asynchronous load implementation, which is the value of next for the

last hook in the load chain, returns null for source when format is

'commonjs' for backward compatibility. Here is an example hook that would

opt-in to using the non-default behavior:

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

// Asynchronous version accepted by module.register(). This fix is not needed

// for the synchronous version accepted by module.registerHooks().

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 包含由下一个钩子加载的源代码,无论模块格式如何。

🌐 This doesn't apply to the synchronous load hook either, in which case the

source returned contains source code loaded by the next hook, regardless

of module format.

示例#>

🌐 Examples

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

🌐 The various module customization hooks can be used together to accomplish

wide-ranging customizations of the Node.js code loading and evaluation

behaviors.

从 HTTPS 导入#>

🌐 Import from HTTPS

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

🌐 The hook below registers hooks to enable rudimentary support for such

specifiers. While this may seem like a significant improvement to Node.js core

functionality, there are substantial downsides to actually using these hooks:

performance is much slower than loading files from disk, there is no caching,

and there is no security.

// https-hooks.mjs

import { get } from 'node:https';

export function load(url, context, nextLoad) {

// For JavaScript to be loaded over the network, we need to fetch and

// return it.

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({

// This example assumes all network-provided JavaScript is ES module

// code.

format: 'module',

shortCircuit: true,

source: data,

}));

}).on('error', (err) => reject(err));

});

}

// Let Node.js handle all other URLs.

return nextLoad(url);

} 拷贝

// main.mjs

import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';

console.log(VERSION); 拷贝

使用前面的 hooks 模块,运行

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 版本。

🌐 With the preceding hooks module, running

node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs

prints the current version of CoffeeScript per the module at the URL in

main.mjs.

转换#>

🌐 Transpilation

Node.js 无法理解的格式的源文件可以使用 load 钩子 转换为 JavaScript。

🌐 Sources that are in formats Node.js doesn't understand can be converted into

JavaScript using the load hook.

这比在运行 Node.js 之前转译源文件的性能要低;转译器钩子应该仅用于开发和测试目的。

🌐 This is less performant than transpiling source files before running Node.js;

transpiler hooks should only be used for development and testing purposes.

异步版本#>

🌐 Asynchronous version

// coffeescript-hooks.mjs

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

import { findPackageJSON } from 'node:module';

import coffeescript from 'coffeescript';

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

export async function load(url, context, nextLoad) {

if (extensionsRegex.test(url)) {

// CoffeeScript files can be either CommonJS or ES modules. Use a custom format

// to tell Node.js not to detect its module type.

const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });

// This hook converts CoffeeScript source code into JavaScript source code

// for all imported CoffeeScript files.

const transformedSource = coffeescript.compile(rawSource.toString(), url);

// To determine how Node.js would interpret the transpilation result,

// search up the file system for the nearest parent package.json file

// and read its "type" field.

return {

format: await getPackageType(url),

shortCircuit: true,

source: transformedSource,

};

}

// Let Node.js handle all other URLs.

return nextLoad(url, context);

}

async function getPackageType(url) {

// `url` is only a file path during the first iteration when passed the

// resolved url from the load() hook

// an actual file path from load() will contain a file extension as it's

// required by the spec

// this simple truthy check for whether `url` contains a file extension will

// work for most projects but does not cover some edge-cases (such as

// extensionless files or a url ending in a trailing space)

const pJson = findPackageJSON(url);

return readFile(pJson, 'utf8')

.then(JSON.parse)

.then((json) => json?.type)

.catch(() => undefined);

} 拷贝

同步版本#>

🌐 Synchronous version

// coffeescript-sync-hooks.mjs

import { readFileSync } from 'node:fs';

import { registerHooks, findPackageJSON } from 'node:module';

import coffeescript from 'coffeescript';

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

function load(url, context, nextLoad) {

if (extensionsRegex.test(url)) {

const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });

const transformedSource = coffeescript.compile(rawSource.toString(), url);

return {

format: getPackageType(url),

shortCircuit: true,

source: transformedSource,

};

}

return nextLoad(url, context);

}

function getPackageType(url) {

const pJson = findPackageJSON(url);

if (!pJson) {

return undefined;

}

try {

const file = readFileSync(pJson, 'utf-8');

return JSON.parse(file)?.type;

} catch {

return undefined;

}

}

registerHooks({ load }); 拷贝

正在运行的钩子#>

🌐 Running hooks

# main.coffee

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}" 拷贝

# scream.coffee

export scream = (str) -> str.toUpperCase() 拷贝

为了运行示例,添加一个包含 CoffeeScript 文件模块类型的 package.json 文件。

🌐 For the sake of running the example, add a package.json file containing the

module type of the CoffeeScript files.

{

"type": "module"

} 拷贝

这仅用于运行示例。在实际的加载器中,即使在 package.json 中没有显式的类型,getPackageType() 也必须能够返回 Node.js 已知的 format,否则 nextLoad 调用将抛出 ERR_UNKNOWN_FILE_EXTENSION(如果未定义)或 ERR_UNKNOWN_MODULE_FORMAT(如果它不是 加载钩子 文档中列出的已知格式)。

🌐 This is only for running the example. In real world loaders, getPackageType() must be

able to return an format known to Node.js even in the absence of an explicit type in a

package.json, or otherwise the nextLoad call would throw ERR_UNKNOWN_FILE_EXTENSION

(if undefined) or ERR_UNKNOWN_MODULE_FORMAT (if it's not a known format listed in

the load hook documentation).

使用前面的钩子模块,运行

node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee

或者 node --import ./coffeescript-sync-hooks.mjs ./main.coffee

会在从磁盘加载 main.coffee 的源代码后,但在 Node.js 执行之前,将其转换为 JavaScript;对于通过 import 语句引用的任何已加载文件的 .coffee、.litcoffee 或 .coffee.md 文件也同样适用。

🌐 With the preceding hooks modules, running

node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee

or node --import ./coffeescript-sync-hooks.mjs ./main.coffee

causes main.coffee to be turned into JavaScript after its source code is

loaded from disk but before Node.js executes it; and so on for any .coffee,

.litcoffee or .coffee.md files referenced via import statements of any

loaded file.

导入映射#>

🌐 Import maps

前两个例子定义了 load 钩子。下面是一个 resolve 钩子的例子。这个钩子模块会读取一个 import-map.json 文件,该文件定义了哪些标识符需要重写为其他 URL(这是对“导入映射”规范中一个小子集的非常简单的实现)。

🌐 The previous two examples defined load hooks. This is an example of a

resolve hook. This hooks module reads an import-map.json file that defines

which specifiers to override to other URLs (this is a very simplistic

implementation of a small subset of the "import maps" specification).

异步版本#>

🌐 Asynchronous version

// 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);

} 拷贝

同步版本#>

🌐 Synchronous version

// 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 }); 拷贝

使用钩子#>

🌐 Using the hooks

有了这些文件:

🌐 With these files:

// main.js

import 'a-module'; 拷贝

// import-map.json

{

"imports": {

"a-module": "./some-module.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.js 或 node --import ./import-map-sync-hooks.js main.js 应该会打印 some module!。

🌐 Running node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.js

or node --import ./import-map-sync-hooks.js main.js

should print some module!.

关键点

定制钩子#> 🌐 Customization Hooks 版本历史 版本变更 v25.4.0, v24.13.1 Synchronous and in-thread hooks are now release candidate. v23.5.0, v22.15.0 Add support for synchronous a