模块自定义挂钩
Deno 支持 Node.js
module.registerHooks()
API,它允许你拦截并自定义模块的解析和加载方式。
这使得你可以在不修改导入代码的情况下,实现虚拟模块、自定义转译、模块别名以及类似
用例。node:module API 是 Deno 更广泛的 Node.js 兼容性 层的一部分。
这些挂钩是同步的,并且在与你的应用程序同一线程中运行。它们既适用于 ES 模块(import)也适用于 CommonJS
(require())。
Deno 不实现异步的
module.register()API。请使用registerHooks()同时进行 CommonJS 和 ESM 自定义。
基本示例 Jump to heading
import { registerHooks } from "node:module";
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "virtual:greet") {
return { url: "file:///virtual_greet.js", shortCircuit: true };
}
return nextResolve(specifier, context);
},
load(url, context, nextLoad) {
if (url === "file:///virtual_greet.js") {
return {
source: 'export const msg = "hello from hooks";',
format: "module",
shortCircuit: true,
};
}
return nextLoad(url, context);
},
});
const { msg } = await import("virtual:greet");
console.log(msg); // "hello from hooks"
// 不再需要时移除挂钩
hooks.deregister();
deno run --allow-all main.mjs
使用 --import 加载挂钩 Jump to heading
为了保持应用代码整洁——并确保在程序中的任何内容导入受其影响的模块之前就已安装挂钩——
将 registerHooks() 调用放在它自己的加载器文件中,并使用 --import 预加载它(
这是 --preload 的别名)。
import { registerHooks } from "node:module";
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "virtual:greet") {
return { url: "file:///virtual_greet.js", shortCircuit: true };
}
return nextResolve(specifier, context);
},
load(url, context, nextLoad) {
if (url === "file:///virtual_greet.js") {
return {
source: 'export const msg = "hello from loader";',
format: "module",
shortCircuit: true,
};
}
return nextLoad(url, context);
},
});
const { msg } = await import("virtual:greet");
console.log(msg); // "hello from loader"
使用指向加载器的 --import 运行:
deno run --import ./loader.mjs main.mjs
--import 接受多个值,因此你可以组合加载器(例如
--import ./aliases.mjs --import ./transpile.mjs)。它们按给定顺序注册,
而这与它们运行顺序相反——参见
挂钩链。该标志适用于
deno run,
deno test,
deno bench,以及
deno serve。
用例 Jump to heading
自定义转译 Jump to heading
即时转换非标准文件格式:
import { registerHooks } from "node:module";
registerHooks({
load(url, context, nextLoad) {
if (url.endsWith(".coffee")) {
const result = nextLoad(url, context);
const compiled = compileCoffeeScript(result.source);
return { source: compiled, format: "module", shortCircuit: true };
}
return nextLoad(url, context);
},
});
模块别名 Jump to heading
将导入重定向到其他模块:
import { registerHooks } from "node:module";
registerHooks({
resolve(specifier, context, nextResolve) {
// 将 lodash 重定向到 lodash-es
if (specifier === "lodash") {
return nextResolve("lodash-es", context);
}
return nextResolve(specifier, context);
},
});
虚拟模块 Jump to heading
创建仅存在于内存中的模块:
import { registerHooks } from "node:module";
const virtualModules = new Map([
["virtual:config", 'export default { debug: true, version: "1.0.0" };'],
["virtual:env", `export const NODE_ENV = "${process.env.NODE_ENV}";`],
]);
registerHooks({
resolve(specifier, context, nextResolve) {
if (virtualModules.has(specifier)) {
return { url: `file:///virtual/${specifier}`, shortCircuit: true };
}
return nextResolve(specifier, context);
},
load(url, context, nextLoad) {
for (const [name, source] of virtualModules) {
if (url === `file:///virtual/${name}`) {
return { source, format: "module", shortCircuit: true };
}
}
return nextLoad(url, context);
},
});
测试时的模拟 Jump to heading
在测试期间用模拟替换模块:
import { registerHooks } from "node:module";
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "./database.js") {
return { url: "file:///mock_database.js", shortCircuit: true };
}
return nextResolve(specifier, context);
},
load(url, context, nextLoad) {
if (url === "file:///mock_database.js") {
return {
source: 'export const query = () => [{ id: 1, name: "mock" }];',
format: "module",
shortCircuit: true,
};
}
return nextLoad(url, context);
},
});
// 运行测试...
hooks.deregister(); // 测试结束后清理
resolve 挂钩 Jump to heading
resolve 挂钩会拦截模块解析,将标识符映射到 URL。
resolve(specifier, context, nextResolve);
参数:
| 参数 | 类型 | 描述 |
|---|---|---|
specifier |
string |
正在解析的模块标识符 |
context |
object |
解析上下文(见下文) |
nextResolve |
function |
委托给下一个挂钩或默认解析器 |
上下文对象:
| 属性 | 类型 | 描述 |
|---|---|---|
conditions |
string[] |
导入条件(例如,ESM 的 ["node", "import"]) |
parentURL |
string |
导入模块的 URL |
importAttributes |
object |
来自 import 语句的导入属性 |
返回值:
| 属性 | 类型 | 描述 |
|---|---|---|
url |
string |
模块解析后的 URL |
shortCircuit |
boolean |
如果为 true,则跳过链中的其余挂钩 |
要么调用 nextResolve() 进行委托,要么返回带有
shortCircuit: true 的结果。你必须二者择一。
load 挂钩 Jump to heading
load 挂钩会拦截模块加载,为已解析的 URL 提供源代码。
load(url, context, nextLoad);
参数:
| 参数 | 类型 | 描述 |
|---|---|---|
url |
string |
已解析的模块 URL |
context |
object |
加载上下文(见下文) |
nextLoad |
function |
委托给下一个挂钩或默认加载器 |
上下文对象:
| 属性 | 类型 | 描述 |
|---|---|---|
format |
string |
模块格式提示(例如 "module"、"commonjs") |
conditions |
string[] |
导入条件 |
importAttributes |
object |
导入属性 |
返回值:
| 属性 | 类型 | 描述 |
|---|---|---|
source |
string | Buffer | null |
模块源代码 |
format |
string |
模块格式:"module"、"commonjs"、"json" |
shortCircuit |
boolean |
如果为 true,则跳过链中的其余挂钩 |
注销挂钩 Jump to heading
registerHooks() 返回一个带有 deregister() 方法的对象,用于移除这些
挂钩:
const hooks = registerHooks({/* ... */});
// 之后,移除挂钩
hooks.deregister();
挂钩链 Jump to heading
你可以注册多个挂钩;它们会形成一个链。挂钩按 LIFO(后注册,先调用)顺序运行,
并且每个挂钩都可以调用 nextResolve() /
nextLoad() 将控制权传递给链中的前一个挂钩:
import { registerHooks } from "node:module";
// 挂钩 1:先注册,后运行
const hook1 = registerHooks({
load(url, context, nextLoad) {
const result = nextLoad(url, context);
if (url.includes("target.js")) {
return {
source: 'export default "from hook1"',
format: "module",
shortCircuit: true,
};
}
return result;
},
});
// 挂钩 2:后注册,先运行
const hook2 = registerHooks({
load(url, context, nextLoad) {
const result = nextLoad(url, context); // 调用 hook1
if (url.includes("target.js")) {
return {
source: 'export default "from hook2"',
format: "module",
shortCircuit: true,
};
}
return result;
},
});
// 结果来自 hook2,因为它先运行(LIFO)
CommonJS Jump to heading
挂钩也会拦截 require():
const { registerHooks } = require("module");
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "virtual-module") {
return { url: "file:///virtual.js", shortCircuit: true };
}
return nextResolve(specifier, context);
},
load(url, context, nextLoad) {
if (url === "file:///virtual.js") {
return {
source: "module.exports = { value: 42 }",
format: "commonjs",
shortCircuit: true,
};
}
return nextLoad(url, context);
},
});
const mod = require("virtual-module");
console.log(mod.value); // 42
hooks.deregister();