Хуки загрузчика
Deno поддерживает API Node.js module.registerHooks(), который позволяет перехватывать и настраивать разрешение и загрузку модулей. Это позволяет использовать виртуальные модули, пользовательскую транспиляцию, псевдонимы модулей и аналогичные решения без изменения импортирующего кода. API node:module является частью более широкого уровня совместимости с Node.js в Deno.
Хуки являются синхронными и выполняются в том же потоке, что и ваше приложение. Они работают как с ES-модулями (import), так и с CommonJS (require()).
Deno не реализует асинхронный API
module.register(). ИспользуйтеregisterHooks()для настройки как CommonJS, так и ESM.
Простой пример
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"
// Remove hooks when no longer needed
hooks.deregister();
deno run --allow-all main.mjs
Загрузка хуков с помощью --import
Чтобы код приложения оставался чистым и хуки устанавливались до того, как какая-либо часть программы импортирует затрагиваемые ими модули, поместите вызов 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.
Варианты использования
Пользовательская транспиляция
Преобразование нестандартных форматов файлов на лету:
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);
},
});
Псевдонимы модулей
Перенаправление импортов на другие модули:
import { registerHooks } from "node:module";
registerHooks({
resolve(specifier, context, nextResolve) {
// Redirect lodash to lodash-es
if (specifier === "lodash") {
return nextResolve("lodash-es", context);
}
return nextResolve(specifier, context);
},
});
Виртуальные модули
Создание модулей, существующих только в памяти:
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);
},
});
Моки для тестов
Замена модулей моками во время тестирования:
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);
},
});
// Run tests...
hooks.deregister(); // Clean up after tests
Внешние зависимости в исходном коде, созданном хуками
Спецификаторы jsr:, npm: и https: не разрешаются автоматически, если они встречаются только в исходном коде, созданном хуком. Это относится к любому хуку load, который возвращает исходный код, не прочитанный Deno с диска самостоятельно, — например, при пользовательской транспиляции, создании виртуальных модулей, моков и т. п.
Deno обнаруживает и устанавливает внешние зависимости, статически анализируя граф модулей до выполнения. Исходный код, возвращаемый хуком load, создаётся во время загрузки, после завершения этого анализа, поэтому любые bare-импорты jsr:, npm: или https:, встречающиеся только в созданном исходном коде, не видны механизму разрешения зависимостей. В результате могут возникнуть ошибки, например Could not find constraint 'lodash-es@latest' in the list of packages.
Чтобы использовать внешнюю зависимость в коде, созданном хуком, объявите её заранее, чтобы она вошла в набор разрешённых пакетов. Например, добавьте её в карту imports в файле deno.json:
{
"imports": {
"lodash-es": "npm:lodash-es@latest"
}
}
и импортируйте её из созданного исходного кода по указанному имени. Это работает именно так, как задумано: разрешение зависимостей детерминировано и управляется lock-файлом, поэтому набор зависимостей должен быть известен до начала выполнения.
Хук resolve
Хук resolve перехватывает разрешение модулей, сопоставляя спецификаторы с URL.
resolve(specifier, context, nextResolve);
Параметры:
| Параметр | Тип | Описание |
|---|---|---|
specifier | string | Разрешаемый спецификатор модуля |
context | object | Контекст разрешения (см. ниже) |
nextResolve | function | Передаёт управление следующему хуку или стандартному механизму разрешения |
Объект контекста:
| Свойство | Тип | Описание |
|---|---|---|
conditions | string[] | Условия импорта (например, ["node", "import"] для ESM) |
parentURL | string | URL импортирующего модуля |
importAttributes | object | Атрибуты импорта из инструкции импорта |
Возвращаемое значение:
| Свойство | Тип | Описание |
|---|---|---|
url | string | Разрешённый URL модуля |
shortCircuit | boolean | Если true, пропустить оставшиеся хуки в цепочке |
Вызовите nextResolve() для передачи управления дальше либо верните результат с shortCircuit: true. Необходимо выполнить одно из этих действий.
Хук load
Хук 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, пропустить оставшиеся хуки в цепочке |
Отмена регистрации хуков
registerHooks() возвращает объект с методом deregister() для удаления хуков:
const hooks = registerHooks({/* ... */});
// Later, remove hooks
hooks.deregister();
Цепочка хуков
Можно зарегистрировать несколько хуков, объединив их в цепочку. Хуки выполняются в порядке LIFO (последний зарегистрированный вызывается первым), и каждый хук может вызвать nextResolve() / nextLoad(), чтобы передать управление предыдущему хуку в цепочке:
import { registerHooks } from "node:module";
// Hook 1: registered first, runs second
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;
},
});
// Hook 2: registered second, runs first
const hook2 = registerHooks({
load(url, context, nextLoad) {
const result = nextLoad(url, context); // Calls hook1
if (url.includes("target.js")) {
return {
source: 'export default "from hook2"',
format: "module",
shortCircuit: true,
};
}
return result;
},
});
// Result comes from hook2 since it runs first (LIFO)
CommonJS
Хуки также перехватывают 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();
Последнее обновление:
© 2018–2025 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/reference/loader_hooks