Внутреннее устройство Webpack построено вокруг библиотеки Tapable, которая реализует систему хуков (hooks) для подключения расширяемой логики на разных этапах компиляции. Хуки позволяют внедряться в жизненный цикл сборки, модифицировать данные, останавливать выполнение, запускать асинхронные операции и строить сложные цепочки плагинов.
Каждый хук в Tapable — это объект с определённой семантикой исполнения: синхронной, асинхронной, каскадной или прерываемой. Тип хуков определяет поведение подписчиков и способ передачи данных между ними.
SyncHook представляет собой базовый тип хуков, где все подписчики выполняются последовательно и синхронно. Возвращаемые значения игнорируются.
Основная характеристика:
Используется там, где требуется просто уведомить подписчиков о событии.
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name"]);
hook.tap("PluginA", (name) => {
console.log("A:", name);
});
hook.tap("PluginB", (name) => {
console.log("B:", name);
});
hook.call("webpack");
Вывод будет строго последовательным:
AsyncHook не является самостоятельной конечной реализацией, а представляет концептуальную основу для асинхронных хуков в Tapable. На практике используются его специализации:
Асинхронность реализуется через:
AsyncSeriesHook выполняет подписчики строго по очереди, ожидая завершения каждого шага перед переходом к следующему.
Поддерживает три формы регистрации:
const { AsyncSeriesHook } = require("tapable");
const hook = new AsyncSeriesHook(["arg"]);
hook.tapAsync("A", (arg, callback) => {
setTimeout(() => {
console.log("A:", arg);
callback();
}, 100);
});
hook.tapAsync("B", (arg, callback) => {
setTimeout(() => {
console.log("B:", arg);
callback();
}, 50);
});
hook.callAsync("data", () => {
console.log("done");
});
Выполнение строго последовательное: сначала A, затем B.
hook.tapPromise("A", (arg) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("A");
resolve();
}, 100);
});
});
AsyncParallelHook запускает все обработчики одновременно и завершает выполнение после завершения всех задач.
Особенности:
const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["task"]);
hook.tapAsync("A", (task, cb) => {
setTimeout(() => {
console.log("A");
cb();
}, 200);
});
hook.tapAsync("B", (task, cb) => {
setTimeout(() => {
console.log("B");
cb();
}, 100);
});
hook.callAsync("job", () => {
console.log("all done");
});
Завершение произойдёт после выполнения всех подписчиков, независимо от порядка их завершения.
WaterfallHook реализует цепочку, где результат каждого обработчика передаётся следующему как входное значение.
Ключевая особенность:
const { WaterfallHook } = require("tapable");
const hook = new WaterfallHook(["value"]);
hook.tap("A", (value) => {
return value + 1;
});
hook.tap("B", (value) => {
return value * 2;
});
const result = hook.call(5);
console.log(result);
Порядок вычисления:
Итог: 12
BailHook прекращает выполнение цепочки, если любой из обработчиков
возвращает значение, отличное от undefined.
Это позволяет реализовать поведение «первого успешного результата».
const { SyncHook, BailHook } = require("tapable");
const hook = new BailHook(["arg"]);
hook.tap("A", (arg) => {
console.log("A");
return null;
});
hook.tap("B", (arg) => {
console.log("B");
return "result from B";
});
hook.tap("C", (arg) => {
console.log("C");
});
const result = hook.call("data");
console.log(result);
Поведение:
В Tapable параметры всегда передаются по фиксированной сигнатуре, заданной при создании хука. Однако интерпретация этих параметров зависит от типа хука:
Внутри Webpack разные типы хуков используются на разных этапах:
Такая модель позволяет гибко балансировать между производительностью и управляемостью потока выполнения.
Каждый хук компилирует список подписчиков в оптимизированную функцию вызова. Это означает:
Различия типов хуков влияют на генерируемую структуру исполнения:
В SyncHook ошибки пробрасываются напрямую и могут остановить выполнение всей цепочки.
В Async хуках обработка ошибок зависит от реализации:
Waterfall и BailHooks не имеют отдельного механизма восстановления после ошибки — выполнение прерывается на уровне вызова.
Выбор конкретного типа хука влияет на:
SyncHook обеспечивает максимальную простоту, AsyncSeriesHook — строгий контроль порядка, AsyncParallelHook — масштабирование, WaterfallHook — трансформацию данных, BailHook — ранний выход при достижении результата.