通过webpack插件,使用阶段式的构建回调,开发者可以webpack构建流程中引入自己操作行为,也就是说我们首先需要了解webpack构建不同阶段中的回调钩子
webpack的构建背景知识
1、webpack的构建流程的下三阶段
* 初始化:启动构建,读取、合并配置文件和shell语句中的配置参数,执行配置文件中的插件实例化语句new Plugin(),实例化全局唯一的Compiler,调用插件的apply 方法点加载Plugin,让插件可以监听相应钩子的调用。
* 编译:根据用户配置的Entry(入口文件)开始,搜索查找入口文件,根据文件类型匹配配置中的Loader,若是同一种文件类型配置了多个loader则从右到左串行调用它们,来对文件内容进行转换,入口文件中的依赖模块会递归的进行同样的处理。
* 输出:将编译后的模块组合成Chunk,将Chunk转换成文件,输出到文件系统中 。
2、compiler和compilation的介绍
**compiler:** 对象代表了完整的webpack环境配置,是webpack的支柱引擎,Compiler对象在启动webpack时被一次性建立(可以理解为Webpack实例,全局唯一),并配置好所有可操作的设置,包括 options,loader 和 plugin。它扩展(extend)自Tapable类,以便注册和调用插件。大多数面向用户的插件,首先会在 Compiler 上注册。当在webpack环境中应用一个插件时,插件将收到此compiler对象的引用,使用它来访问 webpack 的主环境。
**compilation:** 对象代表了一次资源版本构建。当运行webpack开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
编写插件
1、插件的结构
插件是一个构造函数,它的prototype上必须要定义了一个apply方法。这个apply方法在安装插件时,会被 webpack compiler调用一次,同时它接收一个compiler对象的引用,从而使在回调函数中可以通过compiler对象访问webpack的主环境,在功能完成后调用webpack提供的回调。一个简单的插件结构如下:
```
// 构造函数
function RemoveStrictPlugin(options) {
// 使用 options 设置插件实例……
this.options = options;
}
// 在prototype上定义apply 方法,形参为compiler对象
RemoveStrictPlugin.prototype.apply = function(compiler) {
// 将插件注册到compilation钩子
compiler.plugin('compilation', function(compilation) {
// 根据webpack提供api处理相应功能...
// 操作完成后调用 webpack 提供的回调,以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这里而不往后执行
callback();
});
};
module.exports = RemoveStrictPlugin;
```
* compiler.plugin:将插件注册到compilation钩子。
* compilation:常用的一种钩子,编译(compilation)创建之后,执行插件
* callback():插件中若存在异步操作,就需要额外传入一个callback回调函数,并且在插件运行结束时,调用这个callback函数。
2、插件的使用
在webpack配置的 plugin 数组中添的插件实例将会被安装:
var RemoveStrictPlugin = require('remove-strict');
var webpackConfig = {
// ... 这里是其他配置 ...
plugins: [
new RemoveStrictPlugin({options: true})
]
};
根据上面的配置,在webpack的lib/webpack.js中,webpack通过调用插件的apply方法,这也就是为啥我们在开发插件时需要在prototype上定义个apply方法的原因:
```
// 获取配置文件中的plugins属性值
if (options.plugins && Array.isArray(options.plugins)) {
// 遍历配置文件中plugins数组
for (const plugin of options.plugins) {
// 调用每个插件的apply,并传入compiler对象,让插件注册到钩子上
plugin.apply(compiler);
}
}
```
使用此配置文件进行构建的话,会发现控制台中提示:
![image](https://img.58cdn.com.cn/escstatic/fecar/pmuse/chenli03/C0A4BE24-E8F2-4D6D-9AC8-746392A23382.png)
它告诉我们Tabable.plugin这种的调用形式已经被废弃了,请使用新的API,也就是.hooks来替代.plugin这种形式。 .hooks会在后面要说的钩子部分进行讲解。
## 插件机制
了解了插件的编写之后,那webpack实现插件机制是什么呢?大体如下:
「创建」—— webpack在其内部对象上创建各种钩子;
「注册」—— 插件将自己的方法注册到对应钩子上,交给webpack;(也就是编写插件时提到的注册到钩子)
「调用」—— webpack编译过程中,会适时地触发相应钩子,因此也就触发了插件的方法。
#### 钩子的作用
webpack代码中的模块/插件的调用都依赖于钩子,webpack有很多的钩子(180多种),在上面介绍的构建三个阶段中,每个阶段会调用很多的钩子事件,而插件会注册到相应的钩子上(类似与监听事件),当钩子被调用时就会调用注册在该钩子上的插件。webpack的插件机制相较于loader有很大的不同,loader只是固定在转换文件时被调用,而插件可以通过钩子参与的webpack构建的各个环节中,在构建流程中能做的也就更多、更灵活。
#### 钩子的类型
[tapable](https://github.com/webpack/tapable) 这个小型 library 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。webpack中许多对象扩展自 Tapable 类(例如compiler对象)。通过Tapable,可以快速创建各类钩子。以下是各种钩子的类函数:
```
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
```
#### 创建钩子
```
// 引用tapable 使用SyncHook类创建钩子实例
const { SyncHook } = require('tapable');
let sayHook = new SyncHook(['params']);
```
#### 调用钩子
使用call方法调用钩子,这里的.call()的方法是Tapable提供的触发钩子的方法,不是js中原生的call方法。
```
sayHook.call(this.words);
```
#### 注册插件
前面编写插件时我们讲到在apply方法中,通过如下代码可以注册到相应的钩子上:
```
// 将插件注册到compilation钩子
compiler.plugin('compilation', function(compilation) {
// 根据webpack提供api处理相应功能...
// 操作完成后调用 webpack 提供的回调,以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这里而不往后执行
callback();
});
```
而webpack推荐使用新的方式注册钩子:通过钩子类暴露的tap,tapAsync和tapPromise方法,将配置文件中插件注册到相应的钩子上,也就是在构建流程中注入了自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。根据类型的钩子,来可以选择使用tap,tapAsync和tapPromise方法:
```
sayHook.tap('pluginName', compiler => {
console.log('执行插件内容');
});
};
```
那么使用新的方式改写插件代码如下:
```
// 构造函数
function RemoveStrictPlugin(options) {
// 使用 options 设置插件实例……
this.options = options;
}
// 在prototype上定义apply 方法,形参为compiler对象
RemoveStrictPlugin.prototype.apply = function(compiler) {
// 注册compilation事件
compiler.hooks.compilation.tap('myCompilation', function(compilation) {
// 根据webpack提供api处理相应功能...
// 操作完成后调用 webpack 提供的回调,以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这里而不往后执行
callback();
});
};
```
* compiler.hooks:compiler对象上的一个属性,允许我们使用不同的钩子函数。
* .compilation:hooks中常用的一种钩子,编译(compilation)创建之后,执行插件。
* .tap:表示可以注册同步的钩子和异步的钩子,而在此处因为done属于异步AsyncSeriesHook类型的钩子,所以这里表示把插件注册到compilation钩子上。
#### 主要的钩子:
##### compiler的钩子
* entryOption:
>
> SyncBailHook
>
> 在 entry 配置项处理过之后,执行插件。
>
* afterPlugins
> SyncHook
>
> 设置完初始插件之后,执行插件。
>
> 参数:compiler
>
* afterResolvers
> SyncHook
>
> resolver 安装完成之后,执行插件。
>
> 参数:compiler
##### compilation的钩子
* buildModule
> SyncHook
>
> 在模块构建开始之前触发。
>
> 参数:module
>
* rebuildModule
> SyncHook
>
> 在重新构建一个模块之前触发。
>
> 参数:module
>
* failedModule
> SyncHook
>
> 模块构建失败时执行。
>
> 参数:module error
更详细的说明可以参考:[compiler 钩子](https://www.webpackjs.com/api/compiler-hooks/)、[compilation 钩子](https://www.webpackjs.com/api/compilation-hooks/)
---
所以,现在你已经知道开发webpack插件的关键了被?我们想要编写一个插件,只需要这么几步:
1)明确你的插件是要怎么调用的,需不需要传递参数(对应着webpack.config.js中的配置);
2)创建一个构造函数,以此来保证用它能创建一个个插件实例;
3)在构造函数原型对象上定义一个apply方法,并在其中利用tap, tapAsync 和 tapPromise 方法注册我们的自定义插件。