通過webpack插件倘要,使用階段式的構建回調信峻,開發(fā)者可以webpack構建流程中引入自己操作行為敛腌,也就是說我們首先需要了解webpack構建不同階段中的回調鉤子
webpack的構建背景知識
1、webpack的構建流程的下三階段
* 初始化:啟動構建图毕,讀取夷都、合并配置文件和shell語句中的配置參數(shù),執(zhí)行配置文件中的插件實例化語句new Plugin()予颤,實例化全局唯一的Compiler囤官,調用插件的apply 方法點加載Plugin厢破,讓插件可以監(jiān)聽相應鉤子的調用。
* 編譯:根據(jù)用戶配置的Entry(入口文件)開始治拿,搜索查找入口文件,根據(jù)文件類型匹配配置中的Loader笆焰,若是同一種文件類型配置了多個loader則從右到左串行調用它們劫谅,來對文件內容進行轉換,入口文件中的依賴模塊會遞歸的進行同樣的處理嚷掠。
* 輸出:將編譯后的模塊組合成Chunk捏检,將Chunk轉換成文件,輸出到文件系統(tǒng)中 不皆。
2贯城、compiler和compilation的介紹
**compiler:** 對象代表了完整的webpack環(huán)境配置,是webpack的支柱引擎霹娄,Compiler對象在啟動webpack時被一次性建立(可以理解為Webpack實例能犯,全局唯一),并配置好所有可操作的設置犬耻,包括 options踩晶,loader 和 plugin。它擴展(extend)自Tapable類枕磁,以便注冊和調用插件渡蜻。大多數(shù)面向用戶的插件,首先會在 Compiler 上注冊计济。當在webpack環(huán)境中應用一個插件時茸苇,插件將收到此compiler對象的引用,使用它來訪問 webpack 的主環(huán)境沦寂。
**compilation:** 對象代表了一次資源版本構建学密。當運行webpack開發(fā)環(huán)境中間件時,每當檢測到一個文件變化凑队,就會創(chuàng)建一個新的compilation则果,從而生成一組新的編譯資源。一個compilation對象表現(xiàn)了當前的模塊資源漩氨、編譯生成資源西壮、變化的文件、以及被跟蹤依賴的狀態(tài)信息叫惊。compilation對象也提供了很多關鍵時機的回調款青,以供插件做自定義處理時選擇使用。
編寫插件
1霍狰、插件的結構
插件是一個構造函數(shù)抡草,它的prototype上必須要定義了一個apply方法饰及。這個apply方法在安裝插件時,會被 webpack compiler調用一次康震,同時它接收一個compiler對象的引用燎含,從而使在回調函數(shù)中可以通過compiler對象訪問webpack的主環(huán)境,在功能完成后調用webpack提供的回調腿短。一個簡單的插件結構如下:
```
// 構造函數(shù)
function RemoveStrictPlugin(options) {
? // 使用 options 設置插件實例……
? this.options = options;
}
// 在prototype上定義apply 方法屏箍,形參為compiler對象
RemoveStrictPlugin.prototype.apply = function(compiler) {
? ? // 將插件注冊到compilation鉤子
? ? compiler.plugin('compilation', function(compilation) {
? ? ? ? // 根據(jù)webpack提供api處理相應功能...
? ? ? ? // 操作完成后調用 webpack 提供的回調,以通知 Webpack
? ? ? ? // 如果不執(zhí)行 callback橘忱,運行流程將會一直卡在這里而不往后執(zhí)行
? ? ? ? callback();
? ? });
};
module.exports = RemoveStrictPlugin;
```
* compiler.plugin:將插件注冊到compilation鉤子赴魁。
* compilation:常用的一種鉤子,編譯(compilation)創(chuàng)建之后钝诚,執(zhí)行插件
* callback():插件中若存在異步操作颖御,就需要額外傳入一個callback回調函數(shù),并且在插件運行結束時凝颇,調用這個callback函數(shù)潘拱。
2、插件的使用
在webpack配置的 plugin 數(shù)組中添的插件實例將會被安裝:
var RemoveStrictPlugin = require('remove-strict');
var webpackConfig = {
? // ... 這里是其他配置 ...
? plugins: [
? ? new RemoveStrictPlugin({options: true})
? ]
};
根據(jù)上面的配置祈噪,在webpack的lib/webpack.js中泽铛,webpack通過調用插件的apply方法,這也就是為啥我們在開發(fā)插件時需要在prototype上定義個apply方法的原因:
```
// 獲取配置文件中的plugins屬性值
if (options.plugins && Array.isArray(options.plugins)) {
? ? // 遍歷配置文件中plugins數(shù)組
? ? for (const plugin of options.plugins) {
? ? ? ? // 調用每個插件的apply辑鲤,并傳入compiler對象盔腔,讓插件注冊到鉤子上
? ? ? ? plugin.apply(compiler);
? ? }
}
```
使用此配置文件進行構建的話,會發(fā)現(xiàn)控制臺中提示:
![image](https://img.58cdn.com.cn/escstatic/fecar/pmuse/chenli03/C0A4BE24-E8F2-4D6D-9AC8-746392A23382.png)
它告訴我們Tabable.plugin這種的調用形式已經(jīng)被廢棄了月褥,請使用新的API弛随,也就是.hooks來替代.plugin這種形式。 .hooks會在后面要說的鉤子部分進行講解宁赤。
## 插件機制
了解了插件的編寫之后舀透,那webpack實現(xiàn)插件機制是什么呢?大體如下:
「創(chuàng)建」—— webpack在其內部對象上創(chuàng)建各種鉤子决左;
「注冊」—— 插件將自己的方法注冊到對應鉤子上愕够,交給webpack;(也就是編寫插件時提到的注冊到鉤子)
「調用」—— webpack編譯過程中佛猛,會適時地觸發(fā)相應鉤子惑芭,因此也就觸發(fā)了插件的方法。
#### 鉤子的作用
webpack代碼中的模塊/插件的調用都依賴于鉤子继找,webpack有很多的鉤子(180多種)遂跟,在上面介紹的構建三個階段中,每個階段會調用很多的鉤子事件,而插件會注冊到相應的鉤子上(類似與監(jiān)聽事件)幻锁,當鉤子被調用時就會調用注冊在該鉤子上的插件凯亮。webpack的插件機制相較于loader有很大的不同,loader只是固定在轉換文件時被調用哄尔,而插件可以通過鉤子參與的webpack構建的各個環(huán)節(jié)中假消,在構建流程中能做的也就更多、更靈活岭接。
#### 鉤子的類型
[tapable](https://github.com/webpack/tapable) 這個小型 library 是 webpack 的一個核心工具置谦,但也可用于其他地方,以提供類似的插件接口亿傅。webpack中許多對象擴展自 Tapable 類(例如compiler對象)。通過Tapable瘟栖,可以快速創(chuàng)建各類鉤子葵擎。以下是各種鉤子的類函數(shù):
```
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
```
#### 創(chuàng)建鉤子
```
// 引用tapable 使用SyncHook類創(chuàng)建鉤子實例
const { SyncHook } = require('tapable');
let sayHook = new SyncHook(['params']);
```
#### 調用鉤子
使用call方法調用鉤子,這里的.call()的方法是Tapable提供的觸發(fā)鉤子的方法半哟,不是js中原生的call方法酬滤。
```
sayHook.call(this.words);
```
#### 注冊插件
前面編寫插件時我們講到在apply方法中,通過如下代碼可以注冊到相應的鉤子上:
```
// 將插件注冊到compilation鉤子
? ? compiler.plugin('compilation', function(compilation) {
? ? ? ? // 根據(jù)webpack提供api處理相應功能...
? ? ? ? // 操作完成后調用 webpack 提供的回調寓涨,以通知 Webpack
? ? ? ? // 如果不執(zhí)行 callback盯串,運行流程將會一直卡在這里而不往后執(zhí)行
? ? ? ? callback();
? ? });
```
而webpack推薦使用新的方式注冊鉤子:通過鉤子類暴露的tap,tapAsync和tapPromise方法,將配置文件中插件注冊到相應的鉤子上戒良,也就是在構建流程中注入了自定義的構建步驟体捏,這些步驟將在整個編譯過程中不同時機觸發(fā)。根據(jù)類型的鉤子糯崎,來可以選擇使用tap,tapAsync和tapPromise方法:
```
sayHook.tap('pluginName', compiler => {
? ? ? ? console.log('執(zhí)行插件內容');
? ? });
};
```
那么使用新的方式改寫插件代碼如下:
```
// 構造函數(shù)
function RemoveStrictPlugin(options) {
? // 使用 options 設置插件實例……
? this.options = options;
}
// 在prototype上定義apply 方法几缭,形參為compiler對象
RemoveStrictPlugin.prototype.apply = function(compiler) {
? ? // 注冊compilation事件
? ? compiler.hooks.compilation.tap('myCompilation', function(compilation) {
? ? ? ? // 根據(jù)webpack提供api處理相應功能...
? ? ? ? // 操作完成后調用 webpack 提供的回調,以通知 Webpack
? ? ? ? // 如果不執(zhí)行 callback沃呢,運行流程將會一直卡在這里而不往后執(zhí)行
? ? ? ? callback();
? ? });
};
```
* compiler.hooks:compiler對象上的一個屬性年栓,允許我們使用不同的鉤子函數(shù)。
* .compilation:hooks中常用的一種鉤子薄霜,編譯(compilation)創(chuàng)建之后某抓,執(zhí)行插件。
* .tap:表示可以注冊同步的鉤子和異步的鉤子惰瓜,而在此處因為done屬于異步AsyncSeriesHook類型的鉤子妈经,所以這里表示把插件注冊到compilation鉤子上。
#### 主要的鉤子:
##### compiler的鉤子
* entryOption:
>
> SyncBailHook
>
> 在 entry 配置項處理過之后艾凯,執(zhí)行插件扣典。
>
* afterPlugins
> SyncHook
>
> 設置完初始插件之后,執(zhí)行插件。
>
> 參數(shù):compiler
>
* afterResolvers
> SyncHook
>
> resolver 安裝完成之后痹届,執(zhí)行插件呻待。
>
> 參數(shù):compiler
##### compilation的鉤子
* buildModule
> SyncHook
>
> 在模塊構建開始之前觸發(fā)。
>
> 參數(shù):module
>
* rebuildModule
> SyncHook
>
> 在重新構建一個模塊之前觸發(fā)队腐。
>
> 參數(shù):module
>
* failedModule
> SyncHook
>
> 模塊構建失敗時執(zhí)行蚕捉。
>
> 參數(shù):module error
更詳細的說明可以參考:[compiler 鉤子](https://www.webpackjs.com/api/compiler-hooks/)、[compilation 鉤子](https://www.webpackjs.com/api/compilation-hooks/)
---
所以柴淘,現(xiàn)在你已經(jīng)知道開發(fā)webpack插件的關鍵了被迫淹?我們想要編寫一個插件,只需要這么幾步:
1)明確你的插件是要怎么調用的为严,需不需要傳遞參數(shù)(對應著webpack.config.js中的配置)敛熬;
2)創(chuàng)建一個構造函數(shù),以此來保證用它能創(chuàng)建一個個插件實例第股;
3)在構造函數(shù)原型對象上定義一個apply方法应民,并在其中利用tap, tapAsync 和 tapPromise 方法注冊我們的自定義插件。