Node.js v23.5.0 文档


模块:node:module API#

¥Modules: node:module API

Module 对象#

¥The Module object

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

¥Provides general utility methods when interacting with instances of Module, the module variable often seen in CommonJS modules. Accessed via import 'node:module' or require('node:module').

module.builtinModules#

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

¥A list of the names of all modules provided by Node.js. Can be used to verify if a module is maintained by a third party or not.

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

¥module in this context isn't the same object that's provided by the module wrapper. To access it, require the Module module:

// module.mjs
// In an ECMAScript module
import { builtinModules as builtin } from 'node:module';// module.cjs
// In a CommonJS module
const builtin = require('node:module').builtinModules;

module.createRequire(filename)#

  • filename <string> | <URL> 用于构造 require 函数的文件名。必须是文件网址对象、文件网址字符串、或绝对路径字符串。

    ¥filename <string> | <URL> Filename to be used to construct the require function. Must be a file URL object, file URL string, or absolute path string.

  • 返回:<require> require 函数

    ¥Returns: <require> Require function

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

// sibling-module.js is a CommonJS module.
const siblingModule = require('./sibling-module'); 

module.findPackageJSON(specifier[, base])#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active Development

  • specifier <string> | <URL> 要检索其 package.json 的模块的说明符。传递裸说明符时,将返回包根目录下的 package.json。传递相对说明符或绝对说明符时,将返回最近的父 package.json

    ¥specifier <string> | <URL> The specifier for the module whose package.json to retrieve. When passing a bare specifier, the package.json at the root of the package is returned. When passing a relative specifier or an absolute specifier, the closest parent package.json is returned.

  • base <string> | <URL> 包含模块的绝对位置(file: URL 字符串或 FS 路径)。对于 CJS,使用 __filename(而不是 __dirname!);对于 ESM,使用 import.meta.url。如果 specifierabsolute specifier,则无需传递它。

    ¥base <string> | <URL> The absolute location (file: URL string or FS path) of the containing module. For CJS, use __filename (not __dirname!); for ESM, use import.meta.url. You do not need to pass it if specifier is an absolute specifier.

  • 返回:<string> | <undefined> 如果找到 package.json,则为路径。当 startLocation 是包时,包的根 package.json;当相对或未解析时,最接近 startLocationpackage.json

    ¥Returns: <string> | <undefined> A path if the package.json is found. When startLocation is a package, the package's root package.json; when a relative or unresolved, the closest package.json to the startLocation.

警告:请勿使用它来尝试确定模块格式。有很多因素影响该决定;package.json 的 type 字段是最不明确的(例如文件扩展名取代它,加载器钩子取代它)。

¥Caveat: Do not use this to try to determine module format. There are many things effecting that determination; the type field of package.json is the least definitive (ex file extension superceeds it, and a loader hook superceeds that).

/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' 
// /path/to/project/packages/bar/bar.js
import { findPackageJSON } from 'node:module';

findPackageJSON('..', import.meta.url);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
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'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `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'// /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'
// Same result when passing an absolute specifier instead:
findPackageJSON(pathToFileURL(path.join(__dirname, '..')));

findPackageJSON('some-package', __filename);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `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)#

  • moduleName <string> 模块名称

    ¥moduleName <string> name of the module

  • 返回:<boolean> 如果模块是内置的,则返回 true,否则返回 false

    ¥Returns: <boolean> returns true if the module is builtin else returns false

import { isBuiltin } from 'node:module';
isBuiltin('node:fs'); // true
isBuiltin('fs'); // true
isBuiltin('wss'); // false 

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

稳定性: 1.2 - 发布候选

¥Stability: 1.2 - Release candidate

  • specifier <string> | <URL> 需要注册的定制钩子;这应该与传递给 import() 的字符串相同,但如果它是相对的,则它是相对于 parentURL 解析的。

    ¥specifier <string> | <URL> Customization hooks to be registered; this should be the same string that would be passed to import(), except that if it is relative, it is resolved relative to parentURL.

  • parentURL <string> | <URL> 如果你想要相对于基本 URL(例如 import.meta.url)解析 specifier,你可以在此处传递该 URL。默认值:'data:'

    ¥parentURL <string> | <URL> If you want to resolve specifier relative to a base URL, such as import.meta.url, you can pass that URL here. Default: 'data:'

  • options <Object>

    • parentURL <string> | <URL> 如果你想要相对于基本 URL(例如 import.meta.url)解析 specifier,你可以在此处传递该 URL。如果 parentURL 作为第二个参数提供,则忽略此属性。默认值:'data:'

      ¥parentURL <string> | <URL> If you want to resolve specifier relative to a base URL, such as import.meta.url, you can pass that URL here. This property is ignored if the parentURL is supplied as the second argument. Default: 'data:'

    • data <any> 传递到 initialize 钩子的任何任意的、可克隆的 JavaScript 值。

      ¥data <any> Any arbitrary, cloneable JavaScript value to pass into the initialize hook.

    • transferList <Object[]> 可转移对象 要传递到 initialize 钩子中。

      ¥transferList <Object[]> transferable objects to be passed into the initialize hook.

注册一个导出 钩子 的模块,用于自定义 Node.js 模块解析和加载行为。参见 定制钩子

¥Register a module that exports hooks that customize Node.js module resolution and loading behavior. See Customization hooks.

module.registerHooks(options)#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active development

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

¥Register hooks that customize Node.js module resolution and loading behavior. See Customization hooks.

module.stripTypeScriptTypes(code[, options])#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active development

  • code <string> 要从中剥离类型注释的代码。

    ¥code <string> The code to strip type annotations from.

  • options <Object>

    • mode <string> 默认值:'strip'。可能的值是:

      ¥mode <string> Default: 'strip'. Possible values are:

      • 'strip' 仅剥离类型注释,而不执行 TypeScript 特性的转换。

        ¥'strip' Only strip type annotations without performing the transformation of TypeScript features.

      • 'transform' 剥离类型注释并将 TypeScript 功能转换为 JavaScript。

        ¥'transform' Strip type annotations and transform TypeScript features to JavaScript.

    • sourceMap <boolean> 默认值:false。仅当 mode'transform'(如果为 true)时,才会为转换后的代码生成源映射。

      ¥sourceMap <boolean> Default: false. Only when mode is 'transform', if true, a source map will be generated for the transformed code.

    • sourceUrl <string> 指定源映射中使用的源 URL。

      ¥sourceUrl <string> Specifies the source url used in the source map.

  • 返回:<string> 剥离类型注释的代码。module.stripTypeScriptTypes() 从 TypeScript 代码中删除类型注释。它可用于在使用 vm.runInContext()vm.compileFunction() 运行 TypeScript 代码之前从中剥离类型注释。默认情况下,如果代码包含需要转换的 TypeScript 功能(例如 Enums),它将抛出错误,有关更多信息,请参阅 type-stripping。当模式为 'transform' 时,它还会将 TypeScript 功能转换为 JavaScript,有关更多信息,请参阅 转换 TypeScript 功能。当模式为 'strip' 时,不会生成源映射,因为位置被保留了。如果提供了 sourceMap,当模式为 'strip' 时,将引发错误。

    ¥Returns: <string> The code with type annotations stripped. module.stripTypeScriptTypes() removes type annotations from TypeScript code. It can be used to strip type annotations from TypeScript code before running it with vm.runInContext() or vm.compileFunction(). By default, it will throw an error if the code contains TypeScript features that require transformation such as Enums, see type-stripping for more information. When mode is 'transform', it also transforms TypeScript features to JavaScript, see transform TypeScript features for more information. When mode is 'strip', source maps are not generated, because locations are preserved. If sourceMap is provided, when mode is 'strip', an error will be thrown.

WARNING:由于 TypeScript 解析器的变化,此函数的输出不应被视为跨 Node.js 版本的稳定输出。

¥WARNING: The output of this function should not be considered stable across Node.js versions, due to changes in the TypeScript parser.

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

如果提供了 sourceUrl,它将被用作输出末尾的注释:

¥If sourceUrl is provided, it will be used appended as a comment at the end of the output:

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

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

¥When mode is 'transform', the code is transformed to JavaScript:

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);
// Prints:
// var MathUtil;
// (function(MathUtil) {
//     MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...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);
// Prints:
// var MathUtil;
// (function(MathUtil) {
//     MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...

module.syncBuiltinESMExports()#

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

¥The module.syncBuiltinESMExports() method updates all the live bindings for builtin ES Modules to match the properties of the CommonJS exports. It does not add or remove exported names from the ES Modules.

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) => {
  // It syncs the existing readFile property with the new value
  assert.strictEqual(esmFS.readFile, newAPI);
  // readFileSync has been deleted from the required fs
  assert.strictEqual('readFileSync' in fs, false);
  // syncBuiltinESMExports() does not remove readFileSync from esmFS
  assert.strictEqual('readFileSync' in esmFS, true);
  // syncBuiltinESMExports() does not add names
  assert.strictEqual(esmFS.newAPI, undefined);
}); 

模块编译缓存#

¥Module compile cache

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

¥The module compile cache can be enabled either using the module.enableCompileCache() method or the NODE_COMPILE_CACHE=dir environment variable. After it is enabled, whenever Node.js compiles a CommonJS or a ECMAScript Module, it will use on-disk V8 code cache persisted in the specified directory to speed up the compilation. This may slow down the first load of a module graph, but subsequent loads of the same module graph may get a significant speedup if the contents of the modules do not change.

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

¥To clean up the generated compile cache on disk, simply remove the cache directory. The cache directory will be recreated the next time the same directory is used for for compile cache storage. To avoid filling up the disk with stale cache, it is recommended to use a directory under the os.tmpdir(). If the compile cache is enabled by a call to module.enableCompileCache() without specifying the directory, Node.js will use the NODE_COMPILE_CACHE=dir environment variable if it's set, or defaults to path.join(os.tmpdir(), 'node-compile-cache') otherwise. To locate the compile cache directory used by a running Node.js instance, use module.getCompileCacheDir().

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

¥Currently when using the compile cache with V8 JavaScript code coverage, the coverage being collected by V8 may be less precise in functions that are deserialized from the code cache. It's recommended to turn this off when running tests to generate precise coverage.

可以通过 NODE_DISABLE_COMPILE_CACHE=1 环境变量禁用已启用的模块编译缓存。当编译缓存导致意外或不良行为(例如,测试覆盖率较低)时,这可能很有用。

¥The enabled module compile cache can be disabled by the NODE_DISABLE_COMPILE_CACHE=1 environment variable. This can be useful when the compile cache leads to unexpected or undesired behaviors (e.g. less precise test coverage).

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

¥Compilation cache generated by one version of Node.js can not be reused by a different version of Node.js. Cache generated by different versions of Node.js will be stored separately if the same base directory is used to persist the cache, so they can co-exist.

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

¥At the moment, when the compile cache is enabled and a module is loaded afresh, the code cache is generated from the compiled code immediately, but will only be written to disk when the Node.js instance is about to exit. This is subject to change. The module.flushCompileCache() method can be used to ensure the accumulated code cache is flushed to disk in case the application wants to spawn other Node.js instances and let them share the cache long before the parent exits.

module.constants.compileCacheStatus#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active Development

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

¥The following constants are returned as the status field in the object returned by module.enableCompileCache() to indicate the result of the attempt to enable the module compile cache.

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

module.enableCompileCache([cacheDir])#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active Development

  • cacheDir <string> | <undefined> 可选路径,用于指定将存储/检索编译缓存的目录。

    ¥cacheDir <string> | <undefined> Optional path to specify the directory where the compile cache will be stored/retrieved.

  • 返回:<Object>

    ¥Returns: <Object>

    • status <integer> module.constants.compileCacheStatus 之一

      ¥status <integer> One of the module.constants.compileCacheStatus

    • message <string> | <undefined> 如果 Node.js 无法启用编译缓存,则包含错误消息。仅当 statusmodule.constants.compileCacheStatus.FAILED 时才设置。

      ¥message <string> | <undefined> If Node.js cannot enable the compile cache, this contains the error message. Only set if status is module.constants.compileCacheStatus.FAILED.

    • directory <string> | <undefined> 如果启用了编译缓存,则包含存储编译缓存的目录。仅当 statusmodule.constants.compileCacheStatus.ENABLEDmodule.constants.compileCacheStatus.ALREADY_ENABLED 时才设置。

      ¥directory <string> | <undefined> If the compile cache is enabled, this contains the directory where the compile cache is stored. Only set if status is module.constants.compileCacheStatus.ENABLED or module.constants.compileCacheStatus.ALREADY_ENABLED.

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

¥Enable module compile cache in the current Node.js instance.

如果未指定 cacheDir,Node.js 将使用 NODE_COMPILE_CACHE=dir 环境变量指定的目录(如果已设置)或使用 path.join(os.tmpdir(), 'node-compile-cache')。对于一般用例,建议在不指定 cacheDir 的情况下调用 module.enableCompileCache(),以便目录可以在必要时被 NODE_COMPILE_CACHE 环境变量覆盖。

¥If cacheDir is not specified, Node.js will either use the directory specified by the NODE_COMPILE_CACHE=dir environment variable if it's set, or use path.join(os.tmpdir(), 'node-compile-cache') otherwise. For general use cases, it's recommended to call module.enableCompileCache() without specifying the cacheDir, so that the directory can be overridden by the NODE_COMPILE_CACHE environment variable when necessary.

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

¥Since compile cache is supposed to be a quiet optimization that is not required for the application to be functional, this method is designed to not throw any exception when the compile cache cannot be enabled. Instead, it will return an object containing an error message in the message field to aid debugging. If compile cache is enabled successfully, the directory field in the returned object contains the path to the directory where the compile cache is stored. The status field in the returned object would be one of the module.constants.compileCacheStatus values to indicate the result of the attempt to enable the module compile cache.

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

¥This method only affects the current Node.js instance. To enable it in child worker threads, either call this method in child worker threads too, or set the process.env.NODE_COMPILE_CACHE value to compile cache directory so the behavior can be inherited into the child workers. The directory can be obtained either from the directory field returned by this method, or with module.getCompileCacheDir().

module.flushCompileCache()#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active Development

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

¥Flush the module compile cache accumulated from modules already loaded in the current Node.js instance to disk. This returns after all the flushing file system operations come to an end, no matter they succeed or not. If there are any errors, this will fail silently, since compile cache misses should not interfere with the actual operation of the application.

module.getCompileCacheDir()#

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active Development

定制钩子#

¥Customization Hooks

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

¥Stability: 1.2 - Release candidate (asynchronous version) Stability: 1.1 - Active development (synchronous version)

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

¥There are two types of module customization hooks that are currently supported:

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

    ¥module.register(specifier[, parentURL][, options]) which takes a module that exports asynchronous hook functions. The functions are run on a separate loader thread.

  2. module.registerHooks(options) 采用直接在加载模块的线程上运行的同步钩子函数。

    ¥module.registerHooks(options) which takes synchronous hook functions that are run directly on the thread where the module is loaded.

启用#

¥Enabling

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

¥Module resolution and loading can be customized by:

  1. 使用来自 node:moduleregister 方法注册导出一组异步钩子函数的文件,

    ¥Registering a file which exports a set of asynchronous hook functions, using the register method from node:module,

  2. 使用来自 node:moduleregisterHooks 方法注册一组同步钩子函数。

    ¥Registering a set of synchronous hook functions using the registerHooks method from node:module.

可以使用 --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 
// register-hooks.js
// This file can only be require()-ed if it doesn't contain top-level await.
// Use module.register() to register asynchronous hooks in a dedicated thread.
import { register } from 'node:module';
register('./hooks.mjs', import.meta.url);// register-hooks.js
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));
// Use module.registerHooks() to register synchronous hooks in the main thread.
import { registerHooks } from 'node:module';
registerHooks({
  resolve(specifier, context, nextResolve) { /* implementation */ },
  load(url, context, nextLoad) { /* implementation */ },
});// Use module.registerHooks() to register synchronous hooks in the main thread.
const { registerHooks } = require('node:module');
registerHooks({
  resolve(specifier, context, nextResolve) { /* implementation */ },
  load(url, context, nextLoad) { /* implementation */ },
});

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

¥The file passed to --import or --require can also be an export from a dependency:

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

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

¥Where some-package has an "exports" field defining the /register export to map to a file that calls register(), like the following register-hooks.js example.

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

¥Using --import or --require ensures that the hooks are registered before any application files are imported, including the entry point of the application and for any worker threads by default as well.

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

¥Alternatively, register() and registerHooks() can be called from the entry point, though dynamic import() must be used for any ESM code that should be run after the hooks are registered.

import { register } from 'node:module';

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

// Because this is a dynamic `import()`, the `http-to-https` hooks will run
// to handle `./my-app.js` and any other files it imports or requires.
await import('./my-app.js');const { register } = require('node:module');
const { pathToFileURL } = require('node:url');

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

// Because this is a dynamic `import()`, the `http-to-https` hooks will run
// to handle `./my-app.js` and any other files it imports or requires.
import('./my-app.js');

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

¥Customization hooks will run for any modules loaded later than the registration and the modules they reference via import and the built-in require. require function created by users using module.createRequire() can only be customized by the synchronous hooks.

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

¥In this example, we are registering the http-to-https hooks, but they will only be available for subsequently imported modules — in this case, my-app.js and anything it references via import or built-in require in CommonJS dependencies.

如果 import('./my-app.js') 是静态 import './my-app.js',则应用将在注册 http-to-https 钩子之前已加载。这是由于 ES 模块规范,其中静态导入首先从树的叶子进行评估,然后返回主干。my-app.js 内可以有静态导入,直到动态导入 my-app.js 后才会对其求值。

¥If the import('./my-app.js') had instead been a static import './my-app.js', the app would have already been loaded before the http-to-https hooks were registered. This due to the ES modules specification, where static imports are evaluated from the leaves of the tree first, then back to the trunk. There can be static imports within my-app.js, which will not be evaluated until my-app.js is dynamically imported.

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

¥If synchronous hooks are used, both import, require and user require created using createRequire() are supported.

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

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

const require = createRequire(import.meta.url);

// The synchronous hooks affect import, require() and user require() function
// created through createRequire().
await import('./my-app.js');
require('./my-app-2.js');const { register, registerHooks } = require('node:module');
const { pathToFileURL } = require('node:url');

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

const userRequire = createRequire(__filename);

// The synchronous hooks affect import, require() and user require() function
// created through createRequire().
import('./my-app.js');
require('./my-app-2.js');
userRequire('./my-app-3.js');

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

¥Finally, if all you want to do is register hooks before your app runs and you don't want to create a separate file for that purpose, you can pass a data: URL to --import:

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

链接#

¥Chaining

可以多次调用 register

¥It's possible to call register more than once:

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

在这个例子中,注册的钩子将形成链。这些链按后进先出 (LIFO) 方式运行。如果 foo.mjsbar.mjs 都定义了 resolve 钩子,它们将像这样被调用(注意从右到左):节点的默认值 ← ./foo.mjs./bar.mjs (从 ./bar.mjs 开始,然后是 ./foo.mjs,然后是 Node.js 默认值)。这同样适用于所有其他钩子。

¥In this example, the registered hooks will form chains. These chains run last-in, first out (LIFO). If both foo.mjs and bar.mjs define a resolve hook, they will be called like so (note the right-to-left): node's default ← ./foo.mjs./bar.mjs (starting with ./bar.mjs, then ./foo.mjs, then the Node.js default). The same applies to all the other hooks.

注册的钩子也会影响 register 本身。在此示例中,bar.mjs 将通过 foo.mjs 注册的钩子进行解析和加载(因为 foo 的钩子已添加到链中)。这允许用非 JavaScript 语言编写钩子之类的事情,只要早期注册的钩子可以转换为 JavaScript。

¥The registered hooks also affect register itself. In this example, 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 within the module that defines the hooks.

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

¥Chaining of registerHooks work similarly. 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 { registerHooks } from 'node:module';

const hook1 = { /* implementation of hooks */ };
const hook2 = { /* implementation of hooks */ };
// hook2 run before hook1.
registerHooks(hook1);
registerHooks(hook2);// 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);

与模块定制钩子通信#

¥Communication with 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],
});

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

¥Synchronous module hooks are run on the same thread where the application code is run. They can directly mutate the globals of the context accessed by the main thread.

钩子#

¥Hooks

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

¥Asynchronous hooks accepted by module.register()

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.
} 

异步钩子在单独的线程中运行,与运行应用代码的主线程隔离。这意味着它是一个不同的 realm。hooks 线程随时可能被主线程终止,所以不要依赖异步操作(如 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.

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

¥Synchronous hooks accepted by module.registerHooks()

稳定性: 1.1 - 积极开发

¥Stability: 1.1 - Active development

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

¥The module.registerHooks() method accepts synchronous hook functions. initialize() is not supported nor necessary, as the hook implementer can simply run the initialization code directly before the call to module.registerHooks().

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.
} 

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

¥Synchronous hooks are run in the same thread and the same realm where the modules are loaded. Unlike the asynchronous hooks they 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 detail.

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

¥In synchronous hooks, users can expect console.log() to complete in the same way that they expect console.log() in module code to complete.

钩子的约定#

¥Conventions of hooks

钩子是 chain 的一部分,即使该链仅包含一个自定义(用户提供的)钩子和始终存在的默认钩子。钩子函数嵌套:每个函数都必须始终返回一个普通对象,并且链接是由于每个函数调用 next<hookName>() 而发生的,next<hookName>() 是对后续加载器钩子的引用(按 LIFO 顺序)。

¥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 functions nest: each one must always return a plain object, and chaining happens as a result of each function calling next<hookName>(), which is a reference to the subsequent loader's hook (in LIFO order).

返回缺少必需属性的值的钩子会触发异常。没有调用 next<hookName>() 和没有返回 shortCircuit: true 就返回的钩子也会触发异常。这些错误有助于防止链中的意外中断。从钩子返回 shortCircuit: true 表示该链有意在你的钩子处结束。

¥A hook that returns a value lacking a required property triggers an exception. A hook that returns without calling next<hookName>() and without returning shortCircuit: true also triggers an exception. These errors are to help prevent unintentional breaks in the chain. Return shortCircuit: true from a hook to signal that the chain is intentionally ending at your hook.

initialize()#

稳定性: 1.2 - 发布候选

¥Stability: 1.2 - Release candidate

  • data <any> 数据来自 register(loader, import.meta.url, { data })

    ¥data <any> The data from 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 hook 提供了一种定义自定义函数的方法,该函数在 hooks 模块初始化时在 hooks 线程中运行。当 hooks 模块通过 register 注册时,就会进行初始化。

¥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 的返回值可以是 <Promise>,在这种情况下,将在主应用线程恢复执行之前等待它。

¥This hook can receive data from a register invocation, including ports and other transferable objects. The return value of initialize can be a <Promise>, 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)#

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

¥Stability: 1.2 - Release candidate (asynchronous version) Stability: 1.1 - Active development (synchronous version)

  • specifier <string>

  • context <Object>

    • conditions <string[]> 相关 package.json 的导出条件

      ¥conditions <string[]> Export conditions of the relevant package.json

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

      ¥importAttributes <Object> An object whose key-value pairs represent the attributes for the module to import

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

      ¥parentURL <string> | <undefined> The module importing this one, or undefined if this is the Node.js entry point

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

    ¥nextResolve <Function> The subsequent resolve hook in the chain, or the Node.js default resolve hook after the last user-supplied resolve hook

  • 返回:<Object> | <Promise> 异步版本要么采用包含以下属性的对象,要么采用将解析为此类对象的 Promise。同步版本仅接受同步返回的对象。

    ¥Returns: <Object> | <Promise> The asynchronous version takes either an object containing the following properties, or a Promise that will resolve to such an object. The synchronous version only accepts an object returned synchronously.

    • format <string> | <null> | <undefined> 加载钩子的提示(可能会被忽略)'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'

      ¥format <string> | <null> | <undefined> A hint to the load hook (it might be ignored) 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'

    • importAttributes <Object> | <undefined> 缓存模块时要使用的导入属性(可选;如果排除,将使用输入)

      ¥importAttributes <Object> | <undefined> The import attributes to use when caching the module (optional; if excluded the input will be used)

    • shortCircuit <undefined> | <boolean> 此钩子打算终止 resolve 钩子链的信号。默认值:false

      ¥shortCircuit <undefined> | <boolean> A signal that this hook intends to terminate the chain of resolve hooks. Default: false

    • url <string> 此输入解析到的绝对 URL

      ¥url <string> The absolute URL to which this input resolves

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

¥In the case of the asynchronous version, despite support for returning promises and async functions, calls to resolve may still block the main thread which can impact performance.

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

当前的 包导出条件 总是在传入钩子的 context.conditions 数组中。为了保证在调用 defaultResolve 时默认的 Node.js 模块说明符解析行为,传递给它的 context.conditions 数组必须包括最初传递到 resolve 钩子的 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.

// Asynchronous version accepted by module.register().
export async function resolve(specifier, context, nextResolve) {
  const { parentURL = null } = context;

  if (Math.random() > 0.5) { // Some condition.
    // For some or all specifiers, do some custom logic for resolving.
    // Always return an object of the form {url: <string>}.
    return {
      shortCircuit: true,
      url: parentURL ?
        new URL(specifier, parentURL).href :
        new URL(specifier).href,
    };
  }

  if (Math.random() < 0.5) { // Another condition.
    // When calling `defaultResolve`, the arguments can be modified. In this
    // case it's adding another value for matching conditional exports.
    return nextResolve(specifier, {
      ...context,
      conditions: [...context.conditions, 'another-condition'],
    });
  }

  // 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);
} 
// Synchronous version accepted by module.registerHooks().
function resolve(specifier, context, nextResolve) {
  // Similar to the asynchronous resolve() above, since that one does not have
  // any asynchronous logic.
} 

load(url, context, nextLoad)#

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

¥Stability: 1.2 - Release candidate (asynchronous version) Stability: 1.1 - Active development (synchronous version)

  • url <string> resolve 链返回的 URL

    ¥url <string> The URL returned by the resolve chain

  • context <Object>

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

    ¥nextLoad <Function> The subsequent load hook in the chain, or the Node.js default load hook after the last user-supplied load hook

  • 返回:<Object> | <Promise> 异步版本要么采用包含以下属性的对象,要么采用将解析为此类对象的 Promise。同步版本仅接受同步返回的对象。

    ¥Returns: <Object> | <Promise> The asynchronous version takes either an object containing the following properties, or a Promise that will resolve to such an object. The synchronous version only accepts an object returned synchronously.

load 钩子提供了一种方式来定义确定网址应如何解释、检索、以及解析的自定义方法。它还负责验证导入属性。

¥The load hook provides a way to define a custom method of determining how a URL should be interpreted, retrieved, and parsed. It is also in charge of validating the import attributes.

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

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

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

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

¥The value of source is ignored for type 'builtin' because currently it is not possible to replace the value of a Node.js builtin (core) module.

异步 load 钩子中的注意事项#

¥Caveat in the asynchronous load hook

使用异步 load 钩子时,省略与为 'commonjs' 提供 source 的效果非常不同:

¥When using the asynchronous load hook, omitting vs providing a source for 'commonjs' has very different effects:

  • 当提供了 source 时,来自该模块的所有 require 调用都将由具有已注册 resolveload 钩子的 ESM 加载程序处理;来自该模块的所有 require.resolve 调用都将由具有已注册 resolve 钩子的 ESM 加载程序处理;只有 CommonJS API 的子集可用(例如,没有 require.extensions、没有 require.cache、没有 require.resolve.paths),并且 CommonJS 模块加载器上的猴子修补将不适用。

    ¥When a source is provided, all require calls from this module will be processed by the ESM loader with registered resolve and load hooks; all require.resolve calls from this module will be processed by the ESM loader with registered resolve hooks; only a subset of the CommonJS API will be available (e.g. no require.extensions, no require.cache, no require.resolve.paths) and monkey-patching on the CommonJS module loader will not apply.

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

    ¥If source is undefined or null, it will be handled by the CommonJS module loader and require/require.resolve calls will not go through the registered hooks. This behavior for nullish source is temporary — in the future, nullish source will not be supported.

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

¥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' 时,为 source 返回 null,以实现向后兼容。下面是一个选择使用非默认行为的示例钩子:

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

¥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.

警告:异步 load 钩子和 CommonJS 模块的命名空间导出不兼容。尝试将它们一起使用将导致导入的对象为空。这可能会在未来得到解决。这不适用于同步 load 钩子,在这种情况下可以照常使用导出。

¥Warning: The asynchronous load hook and namespaced exports from CommonJS modules are incompatible. Attempting to use them together will result in an empty object from the import. This may be addressed in the future. This does not apply to the synchronous load hook, in which case exports can be used as usual.

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

¥These types all correspond to classes defined in 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.

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

¥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.

// Asynchronous version accepted by module.register().
export async function load(url, context, nextLoad) {
  const { format } = context;

  if (Math.random() > 0.5) { // Some condition
    /*
      For some or all URLs, do some custom logic for retrieving the source.
      Always return an object of the form {
        format: <string>,
        source: <string|buffer>,
      }.
    */
    return {
      format,
      shortCircuit: true,
      source: '...',
    };
  }

  // Defer to the next hook in the chain.
  return nextLoad(url);
} 
// Synchronous version accepted by module.registerHooks().
function load(url, context, nextLoad) {
  // Similar to the asynchronous load() above, since that one does not have
  // any asynchronous logic.
} 

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

¥In a more advanced scenario, this can also be used to transform an unsupported source to a supported one (see Examples below).

示例#

¥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

可以使用 load 钩子 将 Node.js 无法理解的格式的源代码转换为 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 { 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 files can be either CommonJS or ES modules, so we want any
    // CoffeeScript file to be treated by Node.js the same as a .js file at the
    // same location. To determine how Node.js would interpret an arbitrary .js
    // file, search up the file system for the nearest parent package.json file
    // and read its "type" field.
    const format = await getPackageType(url);

    const { source: rawSource } = await nextLoad(url, { ...context, format });
    // This hook converts CoffeeScript source code into JavaScript source code
    // for all imported CoffeeScript files.
    const transformedSource = coffeescript.compile(rawSource.toString(), url);

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

  // Let Node.js handle all other URLs.
  return nextLoad(url);
}

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 isFilePath = !!extname(url);
  // If it is a file path, get the directory it's in
  const dir = isFilePath ?
    dirname(fileURLToPath(url)) :
    url;
  // Compose a file path to a package.json in the same directory,
  // which may or may not exist
  const packagePath = resolvePath(dir, 'package.json');
  // Try to read the possibly nonexistent package.json
  const type = await readFile(packagePath, { encoding: 'utf8' })
    .then((filestring) => JSON.parse(filestring).type)
    .catch((err) => {
      if (err?.code !== 'ENOENT') console.error(err);
    });
  // If package.json existed and contained a `type` field with a value, voilà
  if (type) return type;
  // Otherwise, (if not at the root) continue checking the next directory up
  // If at the root, stop and return false
  return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
} 

同步版本#

¥Synchronous version

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

正在运行的钩子#

¥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() 

使用前面的钩子模块,运行 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 文件,依此类推。

¥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.jsnode --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!.

源映射 v3 支持#

¥Source map v3 support

稳定性: 1 - 实验性的

¥Stability: 1 - Experimental

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

¥Helpers for interacting with the source map cache. This cache is populated when source map parsing is enabled and source map include directives are found in a modules' footer.

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

¥To enable source map parsing, Node.js must be run with the flag --enable-source-maps, or with code coverage enabled by setting NODE_V8_COVERAGE=dir.

// module.mjs
// In an ECMAScript module
import { findSourceMap, SourceMap } from 'node:module';// module.cjs
// In a CommonJS module
const { findSourceMap, SourceMap } = require('node:module');

module.findSourceMap(path)#

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

¥path is the resolved path for the file for which a corresponding source map should be fetched.

类:module.SourceMap#

¥Class: module.SourceMap

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

创建新的 sourceMap 实例。

¥Creates a new sourceMap instance.

payload 是一个对象,其键与 源映射 v3 格式 匹配:

¥payload is an object with keys matching the Source map v3 format:

lineLengths 是生成代码中每行长度的可选数组。

¥lineLengths is an optional array of the length of each line in the generated code.

sourceMap.payload#

用于构造 SourceMap 实例的有效负载的获取器。

¥Getter for the payload used to construct the SourceMap instance.

sourceMap.findEntry(lineOffset, columnOffset)#
  • lineOffset <number> 生成的源中的零索引行号偏移量

    ¥lineOffset <number> The zero-indexed line number offset in the generated source

  • columnOffset <number> 生成的源中的零索引列号偏移量

    ¥columnOffset <number> The zero-indexed column number offset in the generated source

  • 返回:<Object>

    ¥Returns: <Object>

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

¥Given a line offset and column offset in the generated source file, returns an object representing the SourceMap range in the original file if found, or an empty object if not.

返回的对象包含以下键:

¥The object returned contains the following keys:

  • 生成行:<number> 生成的源中范围开始的行偏移量

    ¥generatedLine: <number> The line offset of the start of the range in the generated source

  • 生成的列:<number> 生成的源中范围开始的列偏移量

    ¥generatedColumn: <number> The column offset of start of the range in the generated source

  • 原文出处:<string> 原始源的文件名,如 SourceMap 中报告的那样

    ¥originalSource: <string> The file name of the original source, as reported in the SourceMap

  • 原行:<number> 原始源中范围开始的行偏移量

    ¥originalLine: <number> The line offset of the start of the range in the original source

  • 原文栏目:<number> 原始源中范围开始的列偏移量

    ¥originalColumn: <number> The column offset of start of the range in the original source

  • 名称:<string>

    ¥name: <string>

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

¥The returned value represents the raw range as it appears in the SourceMap, based on zero-indexed offsets, not 1-indexed line and column numbers as they appear in Error messages and CallSite objects.

要从错误堆栈和 CallSite 对象报告的行号和列号中获取相应的 1 索引行号和列号,请使用 sourceMap.findOrigin(lineNumber, columnNumber)

¥To get the corresponding 1-indexed line and column numbers from a lineNumber and columnNumber as they are reported by Error stacks and CallSite objects, use sourceMap.findOrigin(lineNumber, columnNumber)

sourceMap.findOrigin(lineNumber, columnNumber)#
  • lineNumber <number> 生成的源中调用站点的 1 索引行号

    ¥lineNumber <number> The 1-indexed line number of the call site in the generated source

  • columnNumber <number> 生成源中调用站点的 1 索引列号

    ¥columnNumber <number> The 1-indexed column number of the call site in the generated source

  • 返回:<Object>

    ¥Returns: <Object>

给定生成源中调用站点的 1 索引 lineNumbercolumnNumber,在原始源中查找相应的调用站点位置。

¥Given a 1-indexed lineNumber and columnNumber from a call site in the generated source, find the corresponding call site location in the original source.

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

¥If the lineNumber and columnNumber provided are not found in any source map, then an empty object is returned. Otherwise, the returned object contains the following keys:

  • 名称:<string> | <undefined> 源映射中范围的名称(如果提供)

    ¥name: <string> | <undefined> The name of the range in the source map, if one was provided

  • 文件名:<string> 原始源的文件名,如 SourceMap 中报告的那样

    ¥fileName: <string> The file name of the original source, as reported in the SourceMap

  • 调用号码:<number> 原始源中相应调用站点的 1 索引行编号

    ¥lineNumber: <number> The 1-indexed lineNumber of the corresponding call site in the original source

  • 栏目编号:<number> 原始源中相应调用站点的 1 索引列编号

    ¥columnNumber: <number> The 1-indexed columnNumber of the corresponding call site in the original source

Node.js 中文网 - 粤ICP备13048890号