webpack插件浅析

通过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 方法注册我们的自定义插件。