Можно сказать, что Webpack доставляет удовольствие и беспокоит, является мощным, но требует определенных затрат на обучение. Прежде чем изучать механизм плагинов webpack, нам нужно сначала понять одну интересную вещь: механизм плагинов webpack — это скелет всего инструмента webpack, и сам webpack построен с использованием этого механизма плагинов. Поэтому после глубокого понимания механизма плагина webpack будет очень полезно оптимизировать проект.
Чтобы обеспечить актуальность и точность контента, вы можете обратить внимание наличный блог.
Плагин веб-пакета
Давайте посмотрим на применение плагина webpack в проекте.
const MyPlugin = require('myplugin')
const webpack = require('webpack')
webpack({
...,
plugins: [new MyPlugin()]
...,
})
Итак, каким условиям он может соответствовать в качестве плагина для веб-пакетов? В целом, плагины webpack имеют следующие характеристики:
-
Автономный модуль JS, предоставляющий соответствующие функции
-
Метод применения в прототипе функции внедряет объект компилятора.
-
Соответствующий обработчик событий веб-пакета монтируется на объекте компилятора.
-
Скомпилированный объект компиляции можно получить в функции обратного вызова обработчика событий, а соответствующий обратный вызов можно получить, если это асинхронный обработчик.
Давайте посмотрим на код ниже:
function MyPlugin(options) {}
// 2.函数原型上的 apply 方法会注入 compiler 对象
MyPlugin.prototype.apply = function(compiler) {
// 3.compiler 对象上挂载了相应的 webpack 事件钩子 4.事件钩子的回调函数里能拿到编译后的 compilation 对象
compiler.plugin('emit', (compilation, callback) => {
...
})
}
// 1.独立的 JS 模块,暴露相应的函数
module.exports = MyPlugin
Таким образом, намечается основная схема плагина webpack.На данный момент есть несколько вопросов.
- Вопрос 1: Почему метод применения должен быть определен в прототипе функции? читатьисходный кодПозже выяснилось, что исходный код был передан через
plugin.apply()
вызвать плагин.
const webpack = (options, callback) => {
...
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}
-
Вопрос 2: Что такое объект компилятора?
-
Вопрос 3: Что такое обработчик событий в объекте компилятора?
-
Вопрос 4: Какой объект компиляции можно получить в функции обратного вызова обработчика событий?
Эти вопросы также являются подсказками этой статьи, давайте рассмотрим их один за другим.
объект компилятора
Компилятор является объектом редактора веб-пакета.При вызове веб-пакета объект компилятора автоматически инициализируется.исходный кодследующим образом:
// webpack/lib/webpack.js
const Compiler = require("./Compiler")
const webpack = (options, callback) => {
...
options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置参数
let compiler = new Compiler(options.context) // 初始化 compiler 对象,这里 options.context 为 process.cwd()
compiler.options = options // 往 compiler 添加初始化参数
new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 环境相关方法
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}
В конце концов, объект компилятора содержит весь настраиваемый контент веб-пакета.При разработке плагинов мы можем получить весь контент, связанный с основной средой веб-пакета, из объекта компилятора.
объект компиляции
Объект компиляции представляет сборку одной версии и ресурс сборки. При запуске webpack каждый раз, когда обнаруживается изменение файла, будет создаваться новая компиляция, что приводит к новому набору скомпилированных ресурсов. Объект компиляции представляет текущие ресурсы модуля, скомпилированные ресурсы, измененные файлы и информацию о состоянии отслеживаемых зависимостей.
Объединив исходный код, чтобы понять приведенный выше абзац, в первую очередь webpack будет вызываться каждый раз, когда он выполняется.compiler.run()
(местоположение источника), затем проследитьonCompiled функцияВ переданном параметре компиляции можно обнаружить, что компиляция исходит из конструктора Compilation.
// webpack/lib/Compiler.js
const Compilation = require("./Compilation");
newCompilation(params) {
const compilation = new Compilation(this);
...
return compilation;
}
Библиотека Tapable, о которой следует упомянуть
После введения объекта компилятора и объекта компиляции я должен упомянуть, чтоtapableЭта библиотека, эта библиотека предоставляет все методы pub/sub, связанные с событиями. и функцияCompilerи функцияCompilationОба наследуются от Tapable.
хук событий
Ловушка событий на самом деле является функцией жизненного цикла, аналогичной инфраструктуре MVVM, которая может выполнять специальную логическую обработку на определенном этапе. Знание некоторых общих обработчиков событий является предварительным условием для написания плагинов для веб-пакетов.Вот некоторые распространенные обработчики событий и их функции:
крюк | эффект | параметр | Типы |
---|---|---|---|
after-plugins | После настройки набора плагинов инициализации | compiler | sync |
after-resolvers | После установки резольверов | compiler | sync |
run | перед чтением записи | compiler | async |
compile | перед созданием новой компиляции | compilationParams | sync |
compilation | компиляция создана | compilation | sync |
emit | Перед генерацией ресурсов и выводом в каталог | compilation | async |
after-emit | После генерации ресурса и вывода в каталог | compilation | async |
done | полная компиляция | stats | sync |
увидеть полностьюРуководство по официальной документации, во время просмотраСвязанный исходный кодВы также можете более четко увидеть определение каждой ловушки событий.
Анализ процесса плагина
Взяв за пример ловушку emit, давайте проанализируем исходный код вызова плагина:
compiler.plugin('emit', (compilation, callback) => {
// 在生成资源并输出到目录之前完成某些逻辑
})
Вызываемая здесь функция плагина взята из упомянутой выше библиотеки tapable, и ее окончательный стек вызовов указывает на hook.tapAsync(), который похож на EventEmitter on,исходный кодследующим образом:
// Tapable.js
options => {
...
if(hook !== undefined) {
const tapOpt = {
name: options.fn.name || "unnamed compat plugin",
stage: options.stage || 0
};
if(options.async)
hook.tapAsync(tapOpt, options.fn); // 将插件中异步钩子的回调函数注入
else
hook.tap(tapOpt, options.fn);
return true;
}
};
Там, где есть внедрение, должен быть и триггер.В исходном коде ранее внедренное асинхронное событие запускается методом callAsync.callAsync аналогичен эммиту EventEmitter.Связанный исходный кодследующим образом:
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
Некоторые подробные детали здесь не будут раскрываться.Позвольте мне рассказать о двух случаях чтения исходного кода относительно больших проектов.
-
Поймай основную ветку, чтобы прочитать и проигнорировать детали. В противном случае будет потрачено много времени и возникнут разочарования;
-
В сочетании с инструментами отладки для анализа многие моменты легко упустить из виду без инструментов отладки;
Практическая реализация плагина webpack
Объединив анализ вышеизложенных знаний, написать собственный плагин для веб-пакета несложно, ключ заключается в идее. Для того, чтобы подсчитать эффективное использование каждого пакета вебпака в проекте, в forkwebpack-visualizerКод был обновлен на основе ,адрес проекта. Эффект следующий:
Основной код плагина основан на упомянутом выше ловушке emit, а также на компиляторе и объектах компиляции. код показывает, как показано ниже:
class AnalyzeWebpackPlugin {
constructor(opts = { filename: 'analyze.html' }) {
this.opts = opts
}
apply(compiler) {
const self = this
compiler.plugin("emit", function (compilation, callback) {
let stats = compilation.getStats().toJson({ chunkModules: true }) // 获取各个模块的状态
let stringifiedStats = JSON.stringify(stats)
// 服务端渲染
let html = `<!doctype html>
<meta charset="UTF-8">
<title>AnalyzeWebpackPlugin</title>
<style>${cssString}</style>
<div id="App"></div>
<script>window.stats = ${stringifiedStats};</script>
<script>${jsString}</script>
`
compilation.assets[`${self.opts.filename}`] = { // 生成文件路径
source: () => html,
size: () => html.length
}
callback()
})
}
}