Архитектура Webpack построена вокруг системы плагинов и загрузчиков. Любое серьёзное расширение функциональности сборщика связано либо с обработкой модулей через loaders, либо с вмешательством в жизненный цикл компиляции через plugins. По мере развития Webpack API этих механизмов существенно изменялся: часть интерфейсов устарела, некоторые были полностью удалены, появились новые хуки, изменилась модель асинхронности и внутренняя организация компиляции.
Понимание изменений API особенно важно при:
.plugin()В Webpack 1–3 основным способом подключения к жизненному циклу компиляции был метод:
compiler.plugin(name, callback)
Пример:
class MyPlugin {
apply(compiler) {
compiler.plugin('emit', (compilation, callback) => {
console.log('Emit phase');
callback();
});
}
}
Проблемы такого подхода:
Начиная с Webpack 4 ядро полностью перешло на новую систему хуков, основанную на библиотеке Tapable.
Теперь вместо:
compiler.plugin('emit', handler)
используется:
compiler.hooks.emit.tap(...)
или:
compiler.hooks.emit.tapAsync(...)
class MyPlugin {
apply(compiler) {
compiler.hooks.compile.tap(
'MyPlugin',
(params) => {
console.log('Compile started');
}
);
}
}
Первый аргумент — имя плагина.
compiler.hooks.emit.tapAsync(
'MyPlugin',
(compilation, callback) => {
setTimeout(() => {
console.log('Async emit');
callback();
}, 100);
}
);
Webpack 5 активно использует Promise-модель.
compiler.hooks.emit.tapPromise(
'MyPlugin',
async (compilation) => {
await saveAssets();
}
);
Такой подход оказался значительно удобнее:
Система Tapable предоставляет разные типы hook-объектов.
Синхронный вызов всех подписчиков.
new SyncHook(['compilation'])
Асинхронное последовательное выполнение.
new AsyncSeriesHook(['compilation'])
Каждый обработчик ждёт завершения предыдущего.
Параллельное выполнение async-хендлеров.
new AsyncParallelHook(['compilation'])
Используется для независимых задач.
Передача результата между обработчиками.
new SyncWaterfallHook(['source'])
Пример:
hook.tap('A', source => {
return source + 'A';
});
hook.tap('B', source => {
return source + 'B';
});
Останавливают цепочку при возврате значения.
new SyncBailHook(['module'])
Активно применяются внутри resolver-системы.
Объект compiler представляет глобальный процесс сборки.
Содержит:
Compilation — конкретный цикл сборки.
Содержит:
В старых версиях многие операции выполнялись через compiler hooks. В Webpack 5 значительная часть логики переместилась внутрь compilation.
Например:
compiler.hooks.compilation.tap(
'MyPlugin',
(compilation) => {
}
);
Внутри compilation теперь доступны:
compilation.hooks.processAssets
В Webpack 4 активно использовались:
optimize-assets
after-optimize-assets
В Webpack 5 они заменены на:
processAssets
class MyPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap(
'MyPlugin',
(compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'MyPlugin',
stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
},
(assets) => {
console.log(Object.keys(assets));
}
);
}
);
}
}
Webpack 5 ввёл многоступенчатую обработку assets.
Добавление новых ресурсов.
PROCESS_ASSETS_STAGE_ADDITIONS
Предварительная обработка.
PROCESS_ASSETS_STAGE_PRE_PROCESS
Оптимизация ресурсов.
PROCESS_ASSETS_STAGE_OPTIMIZE
Минификация.
PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE
Финализация.
PROCESS_ASSETS_STAGE_SUMMARIZE
Формирование отчётов.
PROCESS_ASSETS_STAGE_REPORT
Старая модель имела ряд проблем:
Stage-модель решила эти ограничения.
Ранее assets были обычными объектами:
compilation.assets['bundle.js'] = {
source() {
return code;
},
size() {
return code.length;
}
};
Webpack 5 использует абстракцию Source.
Пример:
const { RawSource } = webpack.sources;
compilation.emitAsset(
'file.txt',
new RawSource('Hello')
);
compilation.emitAsset(
'data.json',
new RawSource(json)
);
compilation.updateAsset(
'bundle.js',
old => new RawSource(modify(old.source()))
);
compilation.deleteAsset('old.js');
Старая модель:
compilation.assets[name] = ...
создавала проблемы:
Новый API сделал обработку ресурсов декларативной.
Ранее большинство загрузчиков выглядело так:
module.exports = function(source) {
return transform(source);
};
Webpack предоставляет loader context через this.
Пример:
module.exports = function(source) {
console.log(this.resourcePath);
return source;
};
Путь к файлу.
this.resourcePath
Старый механизм параметров.
this.query
Устарел.
Webpack 5 использует:
this.getOptions()
module.exports = function(source) {
const options = this.getOptions();
return transform(source, options);
};
Теперь параметры loader можно валидировать.
const schema = {
type: 'object',
properties: {
minimize: {
type: 'boolean'
}
}
};
const { validate } = require('schema-utils');
module.exports = function(source) {
const options = this.getOptions();
validate(schema, options);
return source;
};
module.exports = function(source) {
const callback = this.async();
process(source, result => {
callback(null, result);
});
};
Webpack 5 лучше поддерживает Promise-подход.
module.exports = async function(source) {
const result = await transform(source);
return result;
};
Pitch-фаза позволяет перехватывать цепочку загрузчиков.
module.exports.pitch = function(
remainingRequest,
precedingRequest,
data
) {
console.log('Pitch');
};
При обычной обработке:
style-loader
css-loader
sass-loader
основная фаза:
sass-loader
css-loader
style-loader
pitch-фаза:
style-loader
css-loader
sass-loader
Ранее большинство loaders использовали:
const loaderUtils = require('loader-utils');
Webpack 5 сократил необходимость этой библиотеки.
Старый код:
const loaderUtils = require('loader-utils');
const options = loaderUtils.getOptions(this);
Новый:
const options = this.getOptions();
Многие helper-функции были признаны лишними.
Часть функциональности перенесена в ядро Webpack.
compiler.resolvers.normal.resolve(...)
compiler.resolverFactory.get('normal')
const resolver = compiler.resolverFactory.get('normal');
resolver.resolve(
{},
context,
request,
{},
callback
);
Webpack 5 значительно переработал dependency graph.
Ранее зависимости были относительно простыми структурами.
Теперь используются:
Webpack 5 внедрил:
Это потребовало более формальной модели зависимостей.
Webpack 5 отказался от большого количества прямых ссылок между модулями.
Теперь используется:
compilation.moduleGraph
const connections =
compilation.moduleGraph.getOutgoingConnections(module);
Отдельная структура для chunk relationships:
compilation.chunkGraph
Парсер стал значительно более модульным.
parser.plugin('call require', ...)
parser.hooks.call
.for('require')
.tap('Plugin', expr => {
});
Tapable ввёл HookMap — динамические наборы hooks.
parser.hooks.evaluate
.for('Identifier')
Webpack предоставляет десятки parser hooks:
Ранее плагины напрямую модифицировали template generation.
Webpack 5 существенно ограничил вмешательство в кодогенерацию.
Вместо прямой генерации строк появился RuntimeModule API.
class MyRuntimeModule extends RuntimeModule {
generate() {
return `
console.log('runtime');
`;
}
}
compilation.addRuntimeModule(
chunk,
new MyRuntimeModule()
);
Старый template API:
Runtime abstraction решила эти проблемы.
Webpack 5 внедрил persistent cache.
Теперь плагины должны учитывать:
compiler.getCache('MyPlugin')
const cache = compiler.getCache('MyPlugin');
const result = await cache.getPromise(identifier);
await cache.storePromise(identifier, etag, data);
Snapshot позволяет отслеживать изменения файловой системы.
compilation.fileSystemInfo.createSnapshot(...)
Плагин обязан сообщать Webpack о зависимостях:
compilation.fileDependencies.add(file);
compilation.contextDependencies.add(dir);
compilation.missingDependencies.add(file);
Webpack 5 внедрил встроенную систему логирования.
console.log('plugin');
const logger =
compiler.getInfrastructureLogger('MyPlugin');
logger.info('Started');
Поддерживаются:
Webpack 5 улучшил диагностику производительности.
hook.tap({
name: 'Plugin',
stage: 100
}, callback);
hook.tap({
name: 'B',
before: 'A'
}, callback);
Tapable поддерживает interceptors.
hook.intercept({
register(tapInfo) {
console.log(tapInfo.name);
return tapInfo;
}
});
Child compilers активно используются:
const childCompiler =
compilation.createChildCompiler(
'child',
outputOptions
);
Watch-система стала более сложной из-за persistent cache и snapshot architecture.
compiler.hooks.watchRun.tapAsync(...)
compiler.hooks.invalid.tap(...)
NormalModuleFactory отвечает за создание модулей.
Webpack 5 расширил количество hooks.
nmf.hooks.beforeResolve.tap(...)
nmf.hooks.resolve.tap(...)
nmf.hooks.afterResolve.tap(...)
nmf.hooks.createModule.tap(...)
Webpack некоторое время поддерживал backward compatibility:
compiler.plugin(...)
Но затем старые API были удалены.
Нельзя использовать:
tap()
для async-логики.
Старые плагины ломаются в Webpack 5.
Строковые parser.plugin(…) более не поддерживаются.
Webpack 5 активно скрывает internals.
compiler.plugin('emit', (compilation, callback) => {
compilation.assets['a.txt'] = {
source() {
return 'hello';
},
size() {
return 5;
}
};
callback();
});
const { RawSource } = webpack.sources;
compiler.hooks.thisCompilation.tap(
'Plugin',
compilation => {
compilation.hooks.processAssets.tap(
{
name: 'Plugin',
stage:
compilation
.PROCESS_ASSETS_STAGE_ADDITIONS
},
() => {
compilation.emitAsset(
'a.txt',
new RawSource('hello')
);
}
);
}
);
Loader Runner также был переработан.
Теперь Webpack:
Webpack 5 внедрил более строгую модель ошибок.
Многие ошибки теперь наследуются от:
WebpackError
compilation.warnings.push(error);
compilation.errors.push(error);
Для внутренней инфраструктуры.
compiler.getInfrastructureLogger()
Для сообщений сборки.
compilation.getLogger()
Webpack 5 хранит метаданные ресурсов.
compilation.emitAsset(
'bundle.js',
source,
{
minimized: true
}
);
Tree shaking стал гораздо глубже интегрирован в plugin system.
Плагины обязаны учитывать:
compilation.hooks.runtimeRequirementInTree.tap(...)
Persistent cache требует сериализации.
makeSerializable(
MyDependency,
'my/dependency'
);
Причины эволюции:
Старые API проектировались для значительно более простого bundling pipeline и перестали соответствовать требованиям современных систем сборки.