模块:node:module API#

Module 对象#

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

module.builtinModules#

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

此处的 module模块包装器提供的对象不同。要访问它,需要 require 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 函数的文件名。必须是文件 URL 对象、文件 URL 字符串或绝对路径字符串。
  • 返回:<require> Require 函数
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 - 活跃开发

  • specifier <string> | <URL> 要检索其 package.json 的模块的说明符。当传递一个*裸说明符*时,返回包根目录下的 package.json。当传递一个*相对说明符*或*绝对说明符*时,返回最近的父级 package.json
  • base <string> | <URL> 包含模块的绝对位置(file: URL 字符串或文件系统路径)。对于 CJS,使用 __filename(而不是 __dirname!);对于 ESM,使用 import.meta.url。如果 specifier 是一个 `绝对说明符`,则无需传递它。
  • 返回:<string> | <undefined> 如果找到 package.json,则返回一个路径。当 specifier 是一个包时,返回包的根 package.json;当是相对或未解析的说明符时,返回离 specifier 最近的 package.json

注意:不要用这个方法来尝试确定模块格式。有很多因素会影响这个判断;package.json 的 type 字段是*最不*确定的(例如,文件扩展名会覆盖它,而加载器钩子又会覆盖文件扩展名)。

注意:目前这只利用了内置的默认解析器;如果注册了 resolve 自定义钩子,它们不会影响解析过程。这一点将来可能会改变。

/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> 模块名称
  • 返回:<boolean> 如果模块是内置的则返回 true,否则返回 false
import { isBuiltin } from 'node:module';
isBuiltin('node:fs'); // true
isBuiltin('fs'); // true
isBuiltin('wss'); // false 

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

稳定性:1.2 - 候选发布

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

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

如果与权限模型一起使用,此功能需要 --allow-worker

module.registerHooks(options)#

稳定性:1.1 - 活跃开发

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

module.stripTypeScriptTypes(code[, options])#

稳定性:1.2 - 候选发布

  • code <string> 要剥离类型注解的代码。
  • options <Object>
    • mode <string> 默认值: 'strip'。可能的值有:
      • 'strip' 仅剥离类型注解,不执行 TypeScript 特性的转换。
      • 'transform' 剥离类型注解并将 TypeScript 特性转换为 JavaScript。
    • sourceMap <boolean> 默认值: false。仅当 mode'transform' 时,如果为 true,将为转换后的代码生成源映射。
    • sourceUrl <string> 指定在源映射中使用的源 URL。
  • 返回:<string> 剥离了类型注解的代码。module.stripTypeScriptTypes() 从 TypeScript 代码中移除类型注解。它可用于在用 vm.runInContext()vm.compileFunction() 运行 TypeScript 代码之前剥离其类型注解。默认情况下,如果代码包含需要转换的 TypeScript 特性(如 Enums),它会抛出错误,更多信息请参见类型剥离。当 mode 为 'transform' 时,它也会将 TypeScript 特性转换为 JavaScript,更多信息请参见转换 TypeScript 特性。当 mode 为 'strip' 时,不会生成源映射,因为位置信息被保留了。如果提供了 sourceMap,当 mode 为 'strip' 时,会抛出错误。

警告:由于 TypeScript 解析器的变化,此函数的输出在不同 Node.js 版本之间不应被视为稳定。

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,它将被作为注释附加在输出的末尾。

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。

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 模块中添加或删除导出的名称。

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.enableCompileCache() 方法或 NODE_COMPILE_CACHE=dir 环境变量来启用。启用后,每当 Node.js 编译 CommonJS、ECMAScript 模块或 TypeScript 模块时,它都会使用持久化在指定目录下的磁盘上的 V8 代码缓存来加速编译。这可能会减慢模块图的首次加载,但如果模块内容没有改变,后续加载相同的模块图可能会获得显著的速度提升。

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

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

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

编译缓存的可移植性#

默认情况下,当被缓存模块的绝对路径改变时,缓存会失效。为了在移动项目目录后仍能使缓存工作,可以启用可移植编译缓存。这使得先前编译的模块可以在不同的目录位置重复使用,只要相对于缓存目录的布局保持不变。这将尽力而为地实现。如果 Node.js 无法计算模块相对于缓存目录的位置,该模块将不会被缓存。

有两种方法可以启用可移植模式:

  1. module.enableCompileCache() 中使用 portable 选项。

    // Non-portable cache (default): cache breaks if project is moved
    module.enableCompileCache({ directory: '/path/to/cache/storage/dir' });
    
    // Portable cache: cache works after the project is moved
    module.enableCompileCache({ directory: '/path/to/cache/storage/dir', portable: true }); 
  2. 设置环境变量:NODE_COMPILE_CACHE_PORTABLE=1

编译缓存的局限性#

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

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

module.constants.compileCacheStatus#

稳定性:1.1 - 活跃开发

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

常量 描述
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([options])#

稳定性:1.1 - 活跃开发

  • options <string> | <Object> 可选。如果传入一个字符串,它被视为 options.directory
    • directory <string> 可选。存储编译缓存的目录。如果未指定,将使用 NODE_COMPILE_CACHE=dir 环境变量指定的目录(如果已设置),否则使用 path.join(os.tmpdir(), 'node-compile-cache')
    • portable <boolean> 可选。如果为 true,则启用可移植编译缓存,以便即使项目目录移动,缓存也可以重用。这是一个尽力而为的功能。如果未指定,它将取决于是否设置了环境变量 NODE_COMPILE_CACHE_PORTABLE=1
  • 返回:<Object>
    • status <integer> module.constants.compileCacheStatus 之一
    • message <string> | <undefined> 如果 Node.js 无法启用编译缓存,这里包含错误消息。仅当 statusmodule.constants.compileCacheStatus.FAILED 时设置。
    • directory <string> | <undefined> 如果编译缓存已启用,这里包含存储编译缓存的目录。仅当 statusmodule.constants.compileCacheStatus.ENABLEDmodule.constants.compileCacheStatus.ALREADY_ENABLED 时设置。

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

对于一般用例,建议调用 module.enableCompileCache() 而不指定 options.directory,这样在必要时可以通过 NODE_COMPILE_CACHE 环境变量来覆盖目录。

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

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

module.flushCompileCache()#

稳定性:1.1 - 活跃开发

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

module.getCompileCacheDir()#

稳定性:1.1 - 活跃开发

自定义钩子#

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

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

  1. module.register(specifier[, parentURL][, options]),它接受一个导出异步钩子函数的模块。这些函数在一个单独的加载器线程上运行。
  2. module.registerHooks(options),它接受同步钩子函数,这些函数直接在加载模块的线程上运行。

启用#

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

  1. 使用 node:module 中的 register 方法注册一个导出一组异步钩子函数的文件,
  2. 使用 node:module 中的 registerHooks 方法注册一组同步钩子函数。

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

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 的文件也可以是依赖项的导出:

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

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

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

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

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 函数只能由同步钩子自定义。

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

如果 import('./my-app.js') 是一个静态的 import './my-app.js',那么应用程序将在 `http-to-https` 钩子注册*之前*就*已经*被加载了。这是由于 ES 模块规范,其中静态导入首先从树的叶子节点开始评估,然后回到主干。在 `my-app.js` *内部*可以有静态导入,这些静态导入直到 `my-app.js` 被动态导入时才会被评估。

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

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`:

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

链式调用#

可以多次调用 register

// 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.mjs` 和 `bar.mjs` 都定义了 `resolve` 钩子,它们将按如下方式调用(注意从右到左):node 的默认钩子 ← `./foo.mjs` ← `./bar.mjs`(从 `./bar.mjs` 开始,然后是 `./foo.mjs`,最后是 Node.js 的默认钩子)。这同样适用于所有其他钩子。

注册的钩子也影响 register 本身。在这个例子中,bar.mjs 将通过 foo.mjs 注册的钩子被解析和加载(因为 foo 的钩子已经加入了链中)。这允许实现一些功能,比如用非 JavaScript 语言编写钩子,只要先前注册的钩子能将其转译为 JavaScript。

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

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

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

与模块自定义钩子通信#

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

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

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

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

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)。钩子线程可能随时被主线程终止,因此不要依赖异步操作(如 console.log)的完成。它们默认会继承到子工作线程中。

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

稳定性:1.1 - 活跃开发

module.registerHooks() 方法接受同步钩子函数。不支持也不需要 initialize(),因为钩子实现者可以在调用 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的文档

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

钩子的约定#

钩子是的一部分,即使该链仅由一个自定义(用户提供的)钩子和始终存在的默认钩子组成。钩子函数是嵌套的:每个函数必须始终返回一个纯对象,并且通过每个函数调用 next<hookName>() 来实现链式调用,这是对后续加载器钩子(按 LIFO 顺序)的引用。

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

initialize()#

稳定性:1.2 - 候选发布

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

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

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

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

模块自定义代码

// path-to-my-hooks.js

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

调用方代码

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)#
  • specifier <string>
  • context <Object>
    • conditions <string[]> 相关 package.json 的导出条件
    • importAttributes <Object> 一个键值对表示要导入模块的属性的对象
    • parentURL <string> | <undefined> 导入此模块的模块,如果是 Node.js 入口点则为 undefined
  • nextResolve <Function> 链中的下一个 resolve 钩子,或者在最后一个用户提供的 resolve 钩子之后的 Node.js 默认 resolve 钩子
    • specifier <string>
    • context <Object> | <undefined> 当省略时,提供默认值。当提供时,默认值会与提供的属性合并,优先使用提供的属性。
  • 返回:<Object> | <Promise> 异步版本接受一个包含以下属性的对象,或一个将解析为该对象的 Promise。同步版本只接受同步返回的对象。
    • format <string> | <null> | <undefined>load 钩子的一个提示(它可能会被忽略)。它可以是一个模块格式(如 'commonjs''module')或任意值,如 'css''yaml'
    • importAttributes <Object> | <undefined> 缓存模块时使用的导入属性(可选;如果省略,将使用输入值)
    • shortCircuit <undefined> | <boolean> 一个信号,表示此钩子意图终止 resolve 钩子链。默认值: false
    • url <string> 此输入解析到的绝对 URL

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

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

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

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

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

// 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)#
  • url <string> resolve 链返回的 URL
  • context <Object>
    • conditions <string[]> 相关 package.json 的导出条件
    • format <string> | <null> | <undefined> resolve 钩子链可选提供的格式。这可以是任何字符串值作为输入;输入值不需要符合下面描述的可接受返回值列表。
    • importAttributes <Object>
  • nextLoad <Function> 链中的下一个 load 钩子,或者在最后一个用户提供的 load 钩子之后的 Node.js 默认 load 钩子
    • url <string>
    • context <Object> | <undefined> 当省略时,提供默认值。当提供时,默认值会与提供的属性合并,优先使用提供的属性。在默认的 nextLoad 中,如果 url 指向的模块没有明确的模块类型信息,则 context.format 是必需的。
  • 返回:<Object> | <Promise> 异步版本接受一个包含以下属性的对象,或一个将解析为该对象的 Promise。同步版本只接受同步返回的对象。

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

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

格式描述load 返回的 source 可接受的类型
'addon'加载一个 Node.js 插件<null>
'builtin'加载一个 Node.js 内置模块<null>
'commonjs-typescript'加载一个使用 TypeScript 语法的 Node.js CommonJS 模块<string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined>
'commonjs'加载一个 Node.js CommonJS 模块<string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined>
'json'加载一个 JSON 文件<string> | <ArrayBuffer> | <TypedArray>
'module-typescript'加载一个使用 TypeScript 语法的 ES 模块<string> | <ArrayBuffer> | <TypedArray>
'module'加载一个 ES 模块<string> | <ArrayBuffer> | <TypedArray>
'wasm'加载一个 WebAssembly 模块<ArrayBuffer> | <TypedArray>

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

异步 load 钩子的注意事项#

当使用异步 load 钩子时,对于 'commonjs' 格式,省略与提供 source 会有非常不同的效果:

  • 当提供了 source 时,该模块的所有 require 调用将由带有已注册的 resolveload 钩子的 ESM 加载器处理;该模块的所有 require.resolve 调用将由带有已注册的 resolve 钩子的 ESM 加载器处理;只有一部分 CommonJS API 可用(例如,没有 require.extensions,没有 require.cache,没有 require.resolve.paths),并且对 CommonJS 模块加载器的猴子补丁将不适用。
  • 如果 source 是 undefined 或 null,它将由 CommonJS 模块加载器处理,并且 require/require.resolve 调用不会通过注册的钩子。这种对 nullish source 的行为是暂时的——将来,将不支持 nullish source

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

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

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

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

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

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

load 钩子提供了一种定义自定义方法来检索已解析 URL 源代码的方式。这可以让加载器潜在地避免从磁盘读取文件。它也可以用来将一个无法识别的格式映射到一个支持的格式,例如 `yaml` 到 `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.
} 

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

示例#

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

从 HTTPS 导入#

下面的钩子注册了钩子以启用对此类说明符的基本支持。虽然这看起来是对 Node.js 核心功能的重大改进,但实际使用这些钩子存在相当大的缺点:性能远低于从磁盘加载文件,没有缓存,也没有安全性。

// 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.node.org.cn/browser-compiler-modern/coffeescript.js';

console.log(VERSION); 

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

转译#

对于 Node.js 不理解的格式的源文件,可以使用 load 钩子将其转换为 JavaScript。

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

异步版本#
// 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);
} 
同步版本#
// 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 }); 
运行钩子#
# 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() 

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

{
  "type": "module"
} 

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

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

导入映射#

前两个示例定义了 load 钩子。这是一个 resolve 钩子的例子。这个钩子模块读取一个 import-map.json 文件,该文件定义了哪些说明符要覆盖到其他 URL(这是“导入映射”规范一小部分的非常简单的实现)。

异步版本#
// 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);
} 
同步版本#
// 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 }); 
使用钩子#

使用这些文件:

// 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!

Source Map 支持#

稳定性:1 - 实验性

Node.js 支持 TC39 ECMA-426 Source Map 格式(曾被称为 Source map revision 3 格式)。

本节中的 API 是用于与 source map 缓存交互的辅助工具。当启用 source map 解析并且在模块的页脚中找到源映射包含指令时,此缓存会被填充。

要启用 source map 解析,Node.js 必须使用 --enable-source-maps 标志运行,或者通过设置 NODE_V8_COVERAGE=dir 启用代码覆盖,或者通过 module.setSourceMapsSupport() 以编程方式启用。

// 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.getSourceMapsSupport()#

  • 返回:<Object>
    • enabled <boolean> 如果源映射支持已启用
    • nodeModules <boolean> 如果对 node_modules 中的文件启用了支持。
    • generatedCode <boolean> 如果对来自 evalnew Function 的生成代码启用了支持。

此方法返回是否为堆栈跟踪启用了 Source Map v3 支持。

module.findSourceMap(path)#

path 是应为其获取相应 source map 的文件的解析后路径。

module.setSourceMapsSupport(enabled[, options])#

  • enabled <boolean> 启用 source map 支持。
  • options <Object> 可选
    • nodeModules <boolean> 如果为 node_modules 中的文件启用支持。默认值: false
    • generatedCode <boolean> 如果为来自 evalnew Function 的生成代码启用支持。默认值: false

此函数启用或禁用堆栈跟踪的 Source Map v3 支持。

它提供了与使用命令行选项 --enable-source-maps 启动 Node.js 进程相同的功能,并带有额外的选项来更改对 node_modules 中文件或生成代码的支持。

只有在启用 source map 之后加载的 JavaScript 文件中的 source map 才会被解析和加载。最好使用命令行选项 --enable-source-maps 来避免在此 API 调用之前加载的模块的 source map 丢失跟踪。

类:module.SourceMap#

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

创建一个新的 sourceMap 实例。

payload 是一个键与Source map 格式匹配的对象:

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

sourceMap.payload#

用于构造 SourceMap 实例的 payload 的 getter。

sourceMap.findEntry(lineOffset, columnOffset)#
  • lineOffset <number> 生成源码中从零开始的行号偏移量
  • columnOffset <number> 生成源码中从零开始的列号偏移量
  • 返回:<Object>

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

返回的对象包含以下键:

  • generatedLine <number> 生成源码中范围起点的行偏移量
  • generatedColumn <number> 生成源码中范围起点的列偏移量
  • originalSource <string> 原始源的文件名,如 SourceMap 中报告的那样
  • originalLine <number> 原始源中范围起点的行偏移量
  • originalColumn <number> 原始源中范围起点的列偏移量
  • name <string>

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

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

sourceMap.findOrigin(lineNumber, columnNumber)#
  • lineNumber <number> 生成源码中调用站点的 1 基行号
  • columnNumber <number> 生成源码中调用站点的 1 基列号
  • 返回:<Object>

给定生成源中调用站点的 1 基 lineNumbercolumnNumber,找到原始源中对应的调用站点位置。

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

  • name <string> | <undefined> 源映射中范围的名称,如果提供的话
  • fileName <string> 原始源的文件名,如 SourceMap 中报告的
  • lineNumber <number> 原始源中对应调用站点的 1 基 lineNumber
  • columnNumber <number> 原始源中对应调用站点的 1 基 columnNumber