Система плагинов Webpack построена вокруг событийной модели и механизма хуков, реализованного через библиотеку Tapable. В основе лежит идея: весь процесс сборки разбит на этапы, а каждый этап предоставляет точки расширения, через которые можно вмешиваться в поведение компилятора.
Ключевые сущности:
Compiler — глобальный объект сборщика, создаётся один раз при запуске Webpack. Управляет всем процессом сборки.
Compilation — объект конкретной сборки. Создаётся при каждой новой компиляции (например, при watch-режиме при изменениях файлов).
Tapable hooks — система событий, включающая sync, async, waterfall и другие типы хуков.
Плагин в Webpack — это класс или функция с методом
apply, который получает экземпляр
compiler.
Любой плагин начинается с реализации метода apply:
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('Сборка завершена');
});
}
}
module.exports = MyPlugin;
Суть конструкции:
apply(compiler) вызывается Webpack при
инициализацииtap регистрирует синхронный обработчик события'MyPlugin' — имя плагина для отладкиНа уровне компилятора доступны ключевые точки:
environment — окружение настроеноcompile — начинается новая компиляцияmake — построение графа модулейemit — перед записью ассетов на дискdone — завершение сборкиПример подключения:
class LogCompilePlugin {
apply(compiler) {
compiler.hooks.compile.tap('LogCompilePlugin', () => {
console.log('Начало компиляции');
});
}
}
compilation предоставляет доступ к результатам сборки:
модулям, чанкам, ассетам.
class ModulesLoggerPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('ModulesLoggerPlugin', (compilation) => {
compilation.hooks.finishModules.tap('ModulesLoggerPlugin', (modules) => {
console.log('Количество модулей:', modules.size);
});
});
}
}
Ключевые возможности:
Одно из основных применений плагинов — генерация файлов.
class FilePlugin {
apply(compiler) {
compiler.hooks.emit.tap('FilePlugin', (compilation) => {
const content = 'console.log("Generated file");';
compilation.assets['generated.js'] = {
source: () => content,
size: () => content.length
};
});
}
}
В момент emit все ассеты уже сформированы, но их можно
изменить или добавить новые.
class ModifyBundlePlugin {
apply(compiler) {
compiler.hooks.emit.tap('ModifyBundlePlugin', (compilation) => {
Object.keys(compilation.assets).forEach((filename) => {
if (filename.endsWith('.js')) {
const originalSource = compilation.assets[filename].source();
const wrapped = `/* patched */\n${originalSource}`;
compilation.assets[filename] = {
source: () => wrapped,
size: () => wrapped.length
};
}
});
});
}
}
Такой подход применяется для минификации, вставки баннеров, аналитики.
Типичный пример — добавление заголовка:
class BannerPlugin {
constructor(options) {
this.banner = options.banner;
}
apply(compiler) {
compiler.hooks.compilation.tap('BannerPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'BannerPlugin',
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS
},
(assets) => {
for (const filename in assets) {
if (filename.endsWith('.js')) {
const asset = compilation.assets[filename];
const content = `${this.banner}\n${asset.source()}`;
compilation.assets[filename] = {
source: () => content,
size: () => content.length
};
}
}
}
);
});
}
}
Некоторые хуки требуют асинхронного выполнения.
class AsyncExamplePlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('AsyncExamplePlugin', (compilation, callback) => {
setTimeout(() => {
console.log('Асинхронная операция завершена');
callback();
}, 1000);
});
}
}
Также существует Promise-версия:
compiler.hooks.emit.tapPromise('AsyncExamplePlugin', async (compilation) => {
await new Promise((resolve) => setTimeout(resolve, 500));
});
Плагины могут вмешиваться в граф зависимостей:
compiler.hooks.compilation.tap('DependencyPlugin', (compilation) => {
compilation.hooks.buildModule.tap('DependencyPlugin', (module) => {
console.log('Сборка модуля:', module.resource);
});
});
Возможности:
Можно генерировать модули без реальных файлов:
class VirtualModulePlugin {
apply(compiler) {
compiler.hooks.compilation.tap('VirtualModulePlugin', (compilation) => {
compilation.hooks.additionalAssets.tap('VirtualModulePlugin', () => {
compilation.emitAsset(
'virtual.js',
{
source: () => 'export const a = 42;',
size: () => 20
}
);
});
});
}
}
Webpack 5 ввёл стадии выполнения хуков:
PROCESS_ASSETS_STAGE_ADDITIONSPROCESS_ASSETS_STAGE_OPTIMIZEPROCESS_ASSETS_STAGE_SUMMARIZEПример:
compilation.hooks.processAssets.tap(
{
name: 'StagePlugin',
stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
},
() => {
console.log('Оптимизация ассетов');
}
);
Это позволяет точно управлять порядком выполнения логики.
Compiler содержит важные параметры:
apply(compiler) {
console.log(compiler.options.mode);
console.log(compiler.context);
}
Использование:
Плагин может влиять на ошибки сборки:
compilation.errors.push(new Error('Кастомная ошибка'));
Также можно отслеживать предупреждения:
compilation.warnings.push(new Error('Предупреждение'));
class ConfigurablePlugin {
constructor(options = {}) {
this.prefix = options.prefix || '';
}
apply(compiler) {
compiler.hooks.emit.tap('ConfigurablePlugin', (compilation) => {
console.log(this.prefix + 'emit phase');
});
}
}
class MultiHookPlugin {
apply(compiler) {
compiler.hooks.compile.tap('MultiHookPlugin', () => {
console.log('compile');
});
compiler.hooks.emit.tap('MultiHookPlugin', () => {
console.log('emit');
});
compiler.hooks.done.tap('MultiHookPlugin', () => {
console.log('done');
});
}
}
compiler.hooks.make.tapAsync('ChainPlugin', (compilation, callback) => {
doAsyncTask()
.then(() => callback())
.catch((err) => callback(err));
});
Ошибки, переданные в callback, прерывают сборку.
Плагины тестируются через запуск compiler вручную:
const webpack = require('webpack');
const config = require('./webpack.config');
const compiler = webpack(config);
compiler.run((err, stats) => {
console.log(stats.toString());
});
Подходы:
compiler.hooks.done.tap('StatsPlugin', (stats) => {
console.log(stats.toJson().modules.length);
});
Stats содержит:
Плагины выполняются в порядке регистрации, но порядок внутри хуков может зависеть от:
compilation.hooks.normalModuleLoader.tap('OverridePlugin', (loaderContext, module) => {
if (module.resource.includes('special')) {
loaderContext._source = 'console.log("replaced")';
}
});
Плагины могут участвовать в кешировании через compilation cache:
compilation.cache.set('key', 'value');
Типичная цепочка:
Каждый этап имеет собственные хуки, позволяя плагинам вмешиваться в процесс на разных уровнях абстракции.