前言
webpack-plugin 向开发者提供了 webpack 引擎中完整的能力。通过插件扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。
与 loader 相同,plugin 的本质也是一个模块(它包含一个apply函数),符合 webpack 的一切皆模块的理念。
工作原理
webpack 就像一条串行的生产线,要经过一系列处理流程后才能将源文件转换成输出结果。在这条生产线上webpack 会把一些关键的流程节点暴露给开发者,这些节点称为 hook(钩子),可以类比于 Vue 的生命周期钩子。
webpack-plugin 通过监听需要关注的 hook,在 hook 中引入自定义的构建行为,就能加入到这条生产线中,去改变生产线的运作。
webpack 通过 Tapable 来组织这条复杂的生产线,Tapable 是 webpack 的核心功能库,它提供了统一的插件hook(钩子)类型定义。同时,为 hook 提供了三个方法注册插件功能:
- tap:注册同步钩子和异步钩子。
- tapAsync:回调方式注册异步钩子。
- tapPromise:Promise 方式注册异步钩子。
plugin示例
以下是一个官方的 plugin 示例:
javascript
// 一个 JavaScript 类
class MyExampleWebpackPlugin {
// 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.hooks.emit.tapAsync(
'MyExampleWebpackPlugin',
(compilation, callback) => {
console.log('这是一个示例插件!');
console.log(
'这里表示了资源的单次构建的 `compilation` 对象:',
compilation
);
// 用 webpack 提供的插件 API 处理构建过程
compilation.addModule(/* ... */);
callback();
}
);
}
}
一个 plugin 必须包含一个apply函数,它有一个参数compiler。compiler是一个包含了完整的 webpack 配置的对象,每次启动 webpack 构建时它都是唯一存在的。
在apply函数中,通过compiler对象监听 emit 这个 hook 上注册了一个异步的方法。
可以在 webpack api 中知道,emit 钩子是一个异步钩子,因此在示例中用到了tapAsync这个方法往里加入了插件功能。
emit hook 回调方法中注入了compilation实例,compilation实例能够访问当前构建时的所有模块和相应的依赖。
前面有提到,hook 的类型定义是由 Tapable 提供的,一共有十几种:
javascript
// https://github.com/webpack/tapable/blob/master/lib/index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
webpack 工作流程
webpack 的执行流程涉及两个关键对象:compiler和compilation,它们贯穿 webpack 打包构建的 整个生命周期。它们在 webpack 工作流程中在不同的工作节点提供 hook,以便让开发者注册插件。
以下是一个 webpack 工作流程的简图:

更多 hooks 信息可查阅官网:compiler-hooks、compilation-hooks
自定义 Plugin
现在,我们动手撸一个自定义的 loader。需求是,分析打包输出的文件大小并生成说明文档。
首先,我们在项目根目录下创建文件夹 plugins,并创建一个 plugin 文件 analyze-plugin.js,同时写入基本内容:
javascript
class AnalyzePlugin {
apply(compiler) {
...
}
}
module.exports = AnalyzePlugin;
我们需要获取最终输出的文件内容,因此选择compiler的emit hook。这是一个异步钩子,我们使用 tapAsync方法注册插件,这种注册方式需要在回调函数中注入callback方法作为插件执行完成的标识。
javascript
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
const { assets } = compilation
let sources = []
sources.push(`# 文件大小分析`)
sources.push('| 文件 | 大小(KB) |')
sources.push('| --- | --- |')
for (const filePath in assets) {
sources.push(`| ${filePath} | ${Number((assets[filePath].size() / 1024).toFixed(2))} |`)
}
// 添加输出资源
const fileContent = sources.join('\n')
const newAsset = {
source: () => fileContent,
size: () => fileContent.length
};
// 使用 compilation.emitAsset 方法添加新资源
compilation.emitAsset('analyze.md', newAsset);
callback()
});
通过compilation对象,获取到即将输出的 assets 内容,对其遍历并按markdown语法拼接分析文档的内容。而后,将分析文档使用添加到打包输出的文件里一并生成。
如果希望更灵活一些,比如可以将输出的文件名、文件标题等信息放在配置中,通过插件的构造函数读取。
javascript
new AnalyzePlugin({
outputFile: 'analyze.md',
title: '分析打包资源大小'
})
javascript
class AnalyzePlugin {
constructor(options = {}) {
// 获取指定分析文件名与文件标题
const { outputFile, title } = options
this.outputFile = outputFile
this.title = title
}
apply(compiler) {
...
}
}
最后,我们执行一下打包命令,查看生成的分析文档。
| 文件 | 大小(KB) |
|---|---|
| static/51d58c07297bb973f805.jpg | 366.95 |
| index.html | 7.79 |
| static/css/app.6087d3e9100e1ed1b996.css | 0.13 |
| static/js/app.769ebb778600c7d87e2f.js | 22.04 |
| static/js/runtime.ef6603f9c6cf8071ccb3.js | 7.43 |
到这,就算是完成了一次自定义 plugin 的开发。如果你有更多的需求,可以继续尝试开发。
Github:webpack-template/plugins⭐⭐⭐
更多开发 plugin 的细节可参考官方网站:webpack.js.org/contribute/...