插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 webpack 构建流程中引入自定义的行为。创建插件比创建 loader 更加高级,因为你需要理解 webpack 底层的特性来处理相应的钩子,所以请做好阅读源码的准备!
webpack 插件由以下组成:
apply
方法。// 一个 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();
}
);
}
}
插件是由「具有 apply
方法的 prototype 对象」所实例化出来的。这个 apply
方法在安装插件时,会被 webpack compiler 调用一次。apply
方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。一个插件结构如下:
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap(
'Hello World Plugin',
(
stats /* 绑定 done 钩子后,stats 会作为参数传入。 */
) => {
console.log('Hello World!');
}
);
}
}
module.exports = HelloWorldPlugin;
然后,要安装这个插件,只需要在你的 webpack 配置的 plugin 数组中添加一个实例:
// webpack.config.js
var HelloWorldPlugin = require('hello-world');
module.exports = {
// ... 这里是其他配置 ...
plugins: [new HelloWorldPlugin({ options: true })],
};
使用 schema-utils
来校验传入插件的选项。这里是个例子:
import { validate } from 'schema-utils';
// 选项对象的 schema
const schema = {
type: 'object',
properties: {
test: {
type: 'string',
},
},
};
export default class HelloWorldPlugin {
constructor(options = {}) {
validate(schema, options, {
name: 'Hello World Plugin',
baseDataPath: 'options',
});
}
apply(compiler) {}
}
在插件开发中最重要的两个资源就是 compiler
和 compilation
对象。理解它们的角色是扩展 webpack 引擎重要的第一步。
class HelloCompilationPlugin {
apply(compiler) {
// 指定一个挂载到 compilation 的钩子,回调函数的参数为 compilation 。
compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
// 现在可以通过 compilation 对象绑定各种钩子
compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
console.log('资源已经优化完毕。');
});
});
}
}
module.exports = HelloCompilationPlugin;
compiler
和 compilation
以及其他重要对象提供的钩子清单,请查看 plugins API 文档。
有些插件钩子是异步的。我们可以像同步方式一样用 tap
方法来绑定,也可以用 tapAsync
或 tapPromise
这两个异步方法来绑定。
当我们用 tapAsync
方法来绑定插件时,_必须_调用函数的最后一个参数 callback
指定的回调函数。
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(
'HelloAsyncPlugin',
(compilation, callback) => {
// 执行某些异步操作...
setTimeout(function () {
console.log('异步任务完成...');
callback();
}, 1000);
}
);
}
}
module.exports = HelloAsyncPlugin;
当我们用 tapPromise
方法来绑定插件时,_必须_返回一个 pormise ,异步任务完成后 resolve 。
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
// 返回一个 pormise ,异步任务完成后 resolve
return new Promise((resolve, reject) => {
setTimeout(function () {
console.log('异步任务完成...');
resolve();
}, 1000);
});
});
}
}
module.exports = HelloAsyncPlugin;
一旦我们可以深入理解 webpack compiler 和每个独立的 compilation,我们依赖 webpack 引擎将有无限多的事可以做。我们可以重新格式化已有的文件,创建衍生的文件,或者制作全新的生成文件。
让我们来写一个简单的示例插件,生成一个叫做 assets.md
的新文件;文件内容是所有构建生成的文件的列表。这个插件大概像下面这样:
class FileListPlugin {
static defaultOptions = {
outputFile: 'assets.md',
};
// 需要传入自定义插件构造函数的任意选项
//(这是自定义插件的公开API)
constructor(options = {}) {
// 在应用默认选项前,先应用用户指定选项
// 合并后的选项暴露给插件方法
// 记得在这里校验所有选项
this.options = { ...FileListPlugin.defaultOptions, ...options };
}
apply(compiler) {
const pluginName = FileListPlugin.name;
// webpack 模块实例,可以通过 compiler 对象访问,
// 这样确保使用的是模块的正确版本
// (不要直接 require/import webpack)
const { webpack } = compiler;
// Compilation 对象提供了对一些有用常量的访问。
const { Compilation } = webpack;
// RawSource 是其中一种 “源码”("sources") 类型,
// 用来在 compilation 中表示资源的源码
const { RawSource } = webpack.sources;
// 绑定到 “thisCompilation” 钩子,
// 以便进一步绑定到 compilation 过程更早期的阶段
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
// 绑定到资源处理流水线(assets processing pipeline)
compilation.hooks.processAssets.tap(
{
name: pluginName,
// 用某个靠后的资源处理阶段,
// 确保所有资源已被插件添加到 compilation
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// "assets" 是一个包含 compilation 中所有资源(assets)的对象。
// 该对象的键是资源的路径,
// 值是文件的源码
// 遍历所有资源,
// 生成 Markdown 文件的内容
const content =
'# In this build:\n\n' +
Object.keys(assets)
.map((filename) => `- ${filename}`)
.join('\n');
// 向 compilation 添加新的资源,
// 这样 webpack 就会自动生成并输出到 output 目录
compilation.emitAsset(
this.options.outputFile,
new RawSource(content)
);
}
);
});
}
}
module.exports = { FileListPlugin };
webpack.config.js
const { FileListPlugin } = require('./file-list-plugin.js');
// 在 webpack 配置中使用自定义的插件:
module.exports = {
// …
plugins: [
// 添加插件,使用默认选项
new FileListPlugin(),
// 或者:
// 使用任意支持的选项
new FileListPlugin({
outputFile: 'my-assets.md',
}),
],
};
这样就生成了一个制定名称的 markdown 文件:
# In this build:
- main.css
- main.js
- index.html
webpack 插件可以按照它所注册的事件分成不同的类型。每一个事件钩子都预先定义为同步、异步、瀑布或并行钩子,钩子在内部用 call/callAsync 方法调用。支持的钩子清单或可被绑定的钩子清单,通常在 this.hooks
属性指定。
例如:
this.hooks = {
shouldEmit: new SyncBailHook(['compilation']),
};
表示唯一支持的钩子是 shouldEmit
,这是一个 SyncBailHook
类型的钩子,传入插件的唯一参数是 compilation
。
支持的各类型钩子:
SyncHook
new SyncHook([params])
定义。tap
方法绑定。call(...params)
方法调用。Bail(保险) Hooks
new SyncBailHook([params])
定义。tap
方法绑定。call(...params)
方法调用。Bail类型的钩子,其插件回调函数是串行调用,任意一个插件回调函数返回非 undefined 值,则停止执行插件,该值作为钩子的返回值。optimizeChunks
,optimizeChunkModules
等很有用的事件都是 SyncBailHooks 。
Waterfall(瀑布) Hooks
new SyncWaterfallHook([params])
定义。tap
方法绑定。call(...params)
方法调用。该类型的插件是一个接一个串行执行,前一个的返回值作为后一个的入参。插件需要考虑执行的顺序,因为后一个插件必须接受前一个插件执行的结果作为入参。第一个插件的参数为 init
。
所以 waterfall 钩子至少要制定一个参数。这种模式用于和 ModuleTemplate
,ChunkTemplate
等 webpack 模板相关的 Tapable 实例。
Async Series(异步串联) Hook
new AsyncSeriesHook([params])
定义。tap
/tapAsync
/tapPromise
方法绑定。callAsync(...params)
方法调用。插件处理函数(handler functions)的参数为所有参数,以及一个签名为 (err?: Error) -> void
的 callback 函数,callback 函数的处理函数按注册顺序执行。callback
在所有处理函数执行完后调用。
这是 emit
和 run
事件的常见使用模式。
Async waterfall(异步瀑布) 插件会用 waterfall 方式异步应用
new AsyncWaterfallHook([params])
定义。tap
/tapAsync
/tapPromise
方法绑定。callAsync(...params)
方法调用。插件处理函数的参数为当前值,以及一个签名为 (err: Error, nextValue: any) -> void
的 callback 函数。调用时 nextValue
是下一个处理函数的当前值。第一个处理函数的当前是只 init
。当所有 handler 都执行后,callback执行,参数为最后一个值。任一个 handler 传入 err
值,停止调用 handler 且 callback 被调用。
这种模式在 before-resolve
和 after-resolve
事件中常见。
Async Series Bail
new AsyncSeriesBailHook([params])
定义。tap
/tapAsync
/tapPromise
方法绑定。callAsync(...params)
方法调用。Async Parallel
new AsyncParallelHook([params])
定义。tap
/tapAsync
/tapPromise
方法绑定。callAsync(...params)
方法调用。Webpack 先应用插件的默认配置,再应用自身的默认配置。这样可以让插件拥有自己的默认配置,同时支持实现创建预设配置的插件。