前言
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/...