Node.js v25.0.0 文件
- Node.js v25.0.0
- 目錄
-
索引
- 斷言測試
- 非同步上下文跟蹤
- 非同步鉤子
- 緩衝區
- C++ 外掛
- 使用 Node-API 的 C/C++ 外掛
- C++ 嵌入器 API
- 子程序
- 叢集
- 命令列選項
- 控制檯
- 加密
- 偵錯程式
- 已棄用的 API
- 診斷通道
- DNS
- 域
- 環境變數
- 錯誤
- 事件
- 檔案系統
- 全域性物件
- HTTP
- HTTP/2
- HTTPS
- 檢查器
- 國際化
- 模組:CommonJS 模組
- 模組:ECMAScript 模組
- 模組:
node:moduleAPI - 模組:包
- 模組:TypeScript
- 網路
- 作業系統
- 路徑
- 效能鉤子
- 許可權
- 程序
- Punycode
- 查詢字串
- 逐行讀取
- REPL
- 報告
- 單一可執行檔案應用
- SQLite
- 流
- 字串解碼器
- 測試執行器
- 定時器
- TLS/SSL
- 跟蹤事件
- TTY
- UDP/資料報
- URL
- 實用工具
- V8
- 虛擬機器
- WASI
- Web Crypto API
- Web Streams API
- 工作執行緒
- Zlib
- 其他版本
- 選項
模組:node:module API#
Module 物件#
- 型別:<Object>
在與 Module 例項互動時提供通用工具方法,Module 例項即 CommonJS 模組中常見的 module 變數。透過 import 'node:module' 或 require('node:module') 訪問。
module.builtinModules#
- 型別:<string[]>
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)#
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])#
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)#
import { isBuiltin } from 'node:module';
isBuiltin('node:fs'); // true
isBuiltin('fs'); // true
isBuiltin('wss'); // false
module.register(specifier[, parentURL][, options])#
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)#
options<Object>load<Function> | <undefined> 參見 load 鉤子。預設值:undefined。resolve<Function> | <undefined> 參見 resolve 鉤子。預設值:undefined。
module.stripTypeScriptTypes(code[, options])#
code<string> 要剝離型別註解的程式碼。options<Object>- 返回:<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 無法計算模組相對於快取目錄的位置,該模組將不會被快取。
有兩種方法可以啟用可移植模式:
-
在
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 });
編譯快取的侷限性#
目前,當將編譯快取與 V8 JavaScript 程式碼覆蓋率 一起使用時,V8 收集的覆蓋率在從程式碼快取反序列化的函式中可能不夠精確。建議在執行測試以生成精確覆蓋率時關閉此功能。
由一個 Node.js 版本生成的編譯快取不能被另一個不同版本的 Node.js 重用。如果使用相同的基礎目錄來持久化快取,不同版本的 Node.js 生成的快取將分別儲存,因此它們可以共存。
module.constants.compileCacheStatus#
以下常量作為 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])#
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 無法啟用編譯快取,這裡包含錯誤訊息。僅當status為module.constants.compileCacheStatus.FAILED時設定。directory<string> | <undefined> 如果編譯快取已啟用,這裡包含儲存編譯快取的目錄。僅當status為module.constants.compileCacheStatus.ENABLED或module.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()#
將當前 Node.js 例項中已載入模組累積的模組編譯快取重新整理到磁碟。此方法在所有重新整理檔案系統操作結束後返回,無論它們是否成功。如果有任何錯誤,它將靜默失敗,因為編譯快取未命中不應干擾應用程式的實際操作。
module.getCompileCacheDir()#
- 返回:<string> | <undefined> 如果模組編譯快取已啟用,則返回其目錄路徑,否則返回
undefined。
自定義鉤子#
目前支援兩種型別的模組自定義鉤子:
module.register(specifier[, parentURL][, options]),它接受一個匯出非同步鉤子函式的模組。這些函式在一個單獨的載入器執行緒上執行。module.registerHooks(options),它接受同步鉤子函式,這些函式直接在載入模組的執行緒上執行。
啟用#
模組的解析和載入可以透過以下方式進行自定義:
- 使用
node:module中的register方法註冊一個匯出一組非同步鉤子函式的檔案, - 使用
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` 被動態匯入時才會被評估。
如果使用同步鉤子,則支援 import、require 和使用者使用 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() 接受的同步鉤子#
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()#
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鉤子鏈。預設值:falseurl<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鏈返回的 URLcontext<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。同步版本只接受同步返回的物件。format<string>shortCircuit<undefined> | <boolean> 一個訊號,表示此鉤子意圖終止load鉤子鏈。預設值:falsesource<string> | <ArrayBuffer> | <TypedArray> Node.js 要評估的原始碼
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呼叫將由帶有已註冊的resolve和load鉤子的 ESM 載入器處理;該模組的所有require.resolve呼叫將由帶有已註冊的resolve鉤子的 ESM 載入器處理;只有一部分 CommonJS API 可用(例如,沒有require.extensions,沒有require.cache,沒有require.resolve.paths),並且對 CommonJS 模組載入器的猴子補丁將不適用。 - 如果
source是 undefined 或null,它將由 CommonJS 模組載入器處理,並且require/require.resolve呼叫不會透過註冊的鉤子。這種對 nullishsource的行為是暫時的——將來,將不支援 nullishsource。
這些注意事項不適用於同步 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 中定義的類。
- 特定的 <ArrayBuffer> 物件是 <SharedArrayBuffer>。
- 特定的 <TypedArray> 物件是 <Uint8Array>。
如果一個基於文字的格式(即 '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.org/browser-compiler-modern/coffeescript.js';
console.log(VERSION);
使用前面的鉤子模組,執行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs 會根據 main.mjs 中 URL 的模組列印當前版本的 CoffeeScript。
轉譯#
對於 Node.js 不理解的格式的原始檔,可以使用 load 鉤子將其轉換為 JavaScript。
這比在執行 Node.js 之前轉譯原始檔效能要差;轉譯器鉤子應僅用於開發和測試目的。
非同步版本#
// 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.coffee 或 node --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.js 或 node --import ./import-map-sync-hooks.js main.js 應該會列印 some module!。
Source Map 支援#
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.findSourceMap(path)#
path<string>- 返回:<module.SourceMap> | <undefined> 如果找到 source map 則返回
module.SourceMap,否則返回undefined。
path 是應為其獲取相應 source map 的檔案的解析後路徑。
module.setSourceMapsSupport(enabled[, options])#
此函式啟用或停用堆疊跟蹤的 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 }])#
payload<Object>lineLengths<number[]>
建立一個新的 sourceMap 例項。
payload 是一個鍵與Source map 格式匹配的物件:
file<string>version<number>sources<string[]>sourcesContent<string[]>names<string[]>mappings<string>sourceRoot<string>
lineLengths 是一個可選的陣列,包含生成程式碼中每行的長度。
sourceMap.findEntry(lineOffset, columnOffset)#
給定生成原始檔中的行偏移量和列偏移量,如果找到,則返回一個表示原始檔案中 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)#
給定生成源中呼叫站點的 1 基 lineNumber 和 columnNumber,找到原始源中對應的呼叫站點位置。
如果提供的 lineNumber 和 columnNumber 在任何源對映中都未找到,則返回一個空物件。否則,返回的物件包含以下鍵:
name<string> | <undefined> 源對映中範圍的名稱,如果提供的話fileName<string> 原始源的檔名,如 SourceMap 中報告的lineNumber<number> 原始源中對應呼叫站點的 1 基 lineNumbercolumnNumber<number> 原始源中對應呼叫站點的 1 基 columnNumber