插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来实现相应的钩子。
一、插件由以下部分构成
1、一个具名 JavaScript 函数
2、在它的原型上定义 apply
方法。
3、指定一个触及到 webpack 本身的 事件钩子。
4、操作 webpack 内部的实例特定数据。
5、在实现功能后调用 webpack 提供的 callback。
// 一个 JavaScript class class MyExampleWebpackPlugin { // 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数 apply(compiler) { // 指定要附加到的事件钩子函数 compiler.hooks.emit.tapAsync( 'MyExampleWebpackPlugin', (compilation, callback) => { console.log('This is an example plugin!'); console.log('Here’s the `compilation` object which represents a single build of assets:', compilation); // 使用 webpack 提供的 plugin API 操作构建结果 compilation.addModule(/* ... */); callback(); } ); } }
二、基本插件架构
插件是由一个构造函数(此构造函数上的 prototype 对象具有 apply
方法)的所实例化出来的。这个 apply
方法在安装插件时,会被 webpack compiler 调用一次。apply
方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。
class HelloWorldPlugin { apply(compiler) { compiler.hooks.done.tap('Hello World Plugin', ( stats /* 在 hook 被触及时,会将 stats 作为参数传入。 */ ) => { console.log('Hello World!'); }); } } module.exports = HelloWorldPlugin;
然后,要使用这个插件,在你的 webpack 配置的 plugins
数组中添加一个实例:
// webpack.config.js var HelloWorldPlugin = require('hello-world'); module.exports = { // ... 这里是其他配置 ... plugins: [new HelloWorldPlugin({ options: true })] };
三、compiler 和 compilation
在插件开发中最重要的两个资源就是 compiler
和 compilation
对象。理解它们的角色是扩展 webpack 引擎重要的第一步。
class HelloCompilationPlugin { apply(compiler) { // tap(触及) 到 compilation hook,而在 callback 回调时,会将 compilation 对象作为参数, compiler.hooks.compilation.tap('HelloCompilationPlugin', compilation => { // 现在,通过 compilation 对象,我们可以 tap(触及) 到各种可用的 hooks 了 compilation.hooks.optimize.tap('HelloCompilationPlugin', () => { console.log('正在优化资源。'); }); }); } } module.exports = HelloCompilationPlugin;
这里列出 compiler
, compilation
和其他重要对象上可用 hooks,请查看 插件 API 文档。
四、异步事件钩子
有些插件 hooks 是异步的。想要 tap(触及) 某些 hooks,我们可以使用同步方式运行的 tap
方法,或者使用异步方式运行的 tapAsync
方法或 tapPromise
方法。
tapAsync
在我们使用 tapAsync
方法 tap 插件时,我们需要调用 callback,此 callback 将作为最后一个参数传入函数。
class HelloAsyncPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('HelloAsyncPlugin', (compilation, callback) => { // 做一些异步的事情…… setTimeout(function() { console.log('Done with async work...'); callback(); }, 1000); }); } } module.exports = HelloAsyncPlugin;
tapPromise
在我们使用 tapPromise
方法 tap 插件时,我们需要返回一个 promise,此 promise 将在我们的异步任务完成时 resolve。
class HelloAsyncPlugin { apply(compiler) { compiler.hooks.emit.tapPromise('HelloAsyncPlugin', compilation => { // 返回一个 Promise,在我们的异步任务完成时 resolve…… return new Promise((resolve, reject) => { setTimeout(function() { console.log('异步工作完成……'); resolve(); }, 1000); }); }); } } module.exports = HelloAsyncPlugin;
五、示例
一旦能我们深入理解 webpack compiler 和每个独立的 compilation,我们就能通过 webpack 引擎本身做到无穷无尽的事情。我们可以重新格式化已有的文件,创建衍生的文件,或者制作全新的生成文件。
我们来写一个简单的示例插件,生成一个叫做 filelist.md
的新文件;文件内容是所有构建生成的文件的列表。这个插件大概像下面这样:
class FileListPlugin { apply(compiler) { // emit 是异步 hook,使用 tapAsync 触及它,还可以使用 tapPromise/tap(同步) compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => { // 在生成文件中,创建一个头部字符串: var filelist = 'In this build: '; // 遍历所有编译过的资源文件, // 对于每个文件名称,都添加一行内容。 for (var filename in compilation.assets) { filelist += '- ' + filename + ' '; } // 将这个列表作为一个新的文件资源,插入到 webpack 构建中: compilation.assets['filelist.md'] = { source: function() { return filelist; }, size: function() { return filelist.length; } }; callback(); }); } } module.exports = FileListPlugin;