前面一些小節中,有一些相對復雜一點的構建功能,例如分離 CSS 代碼文件等,都是通過 webpack 的插件來實現的,webpack 強大擴展性的基礎就是它的插件機制。當我們需要一個構建功能是 webpack 本身暫未支持的,我們便可以通過尋找合適的 webpack 插件來幫助實現需要的功能,或者我們也可以嘗試自己開發一個 webpack 插件來滿足項目的構建需求,這一小節會介紹如何開發一個 webpack 插件。
## 一個簡單的 plugin
plugin 的實現可以是一個類,使用時傳入相關配置來創建一個實例,然后放到配置的 `plugins` 字段中,而 plugin 實例中最重要的方法是 `apply`,該方法在 webpack compiler 安裝插件時會被調用一次,`apply` 接收 webpack compiler 對象實例的引用,你可以在 compiler 對象實例上注冊各種事件鉤子函數,來影響 webpack 的所有構建流程,以便完成更多其他的構建任務。
下邊的這個例子,是一個可以創建 webpack 構建文件列表 markdown 的 plugin,實現上相對簡單,但呈現了一個 webpack plugin 的基本形態。
```
class FileListPlugin {
constructor(options) {}
apply(compiler) {
// 在 compiler 的 emit hook 中注冊一個方法,當 webpack 執行到該階段時會調用這個方法
compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
// 給生成的 markdown 文件創建一個簡單標題
var filelist = 'In this build:\n\n'
// 遍歷所有編譯后的資源,每一個文件添加一行說明
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n')
}
// 將列表作為一個新的文件資源插入到 webpack 構建結果中
compilation.assets['filelist.md'] = {
source: function() {
return filelist
},
size: function() {
return filelist.length
},
}
})
}
}
module.exports = FileListPlugin
```
webpack 4.0 版本之前使用的是舊版本的 [tapable](https://github.com/webpack/tapable/tree/tapable-0.2),API 和新版本的差別很大,但是事件鉤子基本還是那一些,只是注冊的方式有了變化,現在官方關于 plugin 新版本的文檔還沒有出來,對于各個鉤子返回什么數據,調整后的影響,我們可以在 3.x 版本的官方文檔基礎上合理猜測,然后編碼測試結果。
## 開發和調試 plugin
你要在本地開發和調試 webpack plugin 是很容易的一件事情,你只需要創建一個 js 代碼文件,如同上述的例子一樣,該文件對外暴露一個類,然后在 webpack 配置文件中引用這個文件的代碼,照樣運行 webpack 構建查看結果即可。大概的配置方式如下:
```
// 假設我們上述那個例子的代碼是 ./plugins/FileListPlugin 這個文件
const FileListPlugin = require('./plugins/FileListPlugin.js')
module.exports = {
// ... 其他配置
plugins: [
new FileListPlugin(), // 實例化這個插件,有的時候需要傳入對應的配置
],
}
```
webpack 是基于 Node.js 開發的,plugin 也不例外,所以 plugin 的調試和調試 Node.js 代碼并無兩樣,簡單的使用 `console` 來打印相關信息,復雜一點的使用斷點,或者利用編輯器提供的功能,例如 [VSCode](https://code.visualstudio.com/) 的 DEBUG,對于這一部分內容,有興趣的同學可以去查找相關資料,不再展開。
## webpack 中的事件鉤子
當開發 plugin 需要時,我們可以查閱官方文檔中提供的事件鉤子列表:[compiler 的事件鉤子](https://doc.webpack-china.org/api/compiler/#%E4%BA%8B%E4%BB%B6%E9%92%A9%E5%AD%90) 和 [compilation 的事件鉤子](https://doc.webpack-china.org/api/compilation/)。
或者查看源碼:[compiler hooks](https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L29) 和 [compilation hooks](https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L91) 來尋找更加詳細的信息。
我們可以看到在事件鉤子列表中看到,webpack 中會有相當多的事件鉤子,基本覆蓋了 webpack 構建流程中的每一個步驟,你可以在這些步驟都注冊自己的處理函數,來添加額外的功能,這就是 webpack 提供的 plugin 擴展。
如果你查看了前面 compiler hooks 或者 compilation hooks 的源碼鏈接,你會看到事件鉤子是這樣聲明的:
```
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]), // 這里的聲明的事件鉤子函數接收的參數是 compilation,
done: new AsyncSeriesHook(["stats"]), // 這里接收的參數是 stats,以此類推
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compilation"]),
run: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
// ...
};
```
從這里你可以看到各個事件鉤子函數接收的參數是什么,你還會發現事件鉤子會有不同的類型,例如 `SyncBailHook`,`AsyncSeriesHook`,`SyncHook`,接下來我們再介紹一下事件鉤子的類型以及我們可以如何更好地利用各種事件鉤子的類型來開發我們需要的 plugin。
## 了解事件鉤子類型
上述提到的 webpack compiler 中使用了多種類型的事件鉤子,根據其名稱就可以區分出是同步還是異步的,對于同步的事件鉤子來說,注冊事件的方法只有 `tap` 可用,例如上述的 `shouldEmit` 應該這樣來注冊事件函數的:
```
apply(compiler) {
compiler.hooks.shouldEmit.tap('PluginName', (compilation) => { /* ... */ })
}
```
但如果是異步的事件鉤子,那么可以使用 `tapPromise` 或者 `tapAsync` 來注冊事件函數,`tapPromise` 要求方法返回 `Promise` 以便處理異步,而 `tapAsync` 則是需要用 `callback` 來返回結果,例如:
```
compiler.hooks.done.tapPromise('PluginName', (stats) => {
// 返回 promise
return new Promise((resolve, reject) => {
// 這個例子是寫一個記錄 stats 的文件
fs.writeFile('path/to/file', stats.toJson(), (err) => err ? reject(err) : resolve())
})
})
// 或者
compiler.hooks.done.tapAsync('PluginName', (stats, callback) => {
// 使用 callback 來返回結果
fs.writeFile('path/to/file', stats.toJson(), (err) => callback(err))
})
// 如果插件處理中沒有異步操作要求的話,也可以用同步的方式
compiler.hooks.done.tap('PluginName', (stats, callback) => {
callback(fs.writeFileSync('path/to/file', stats.toJson())
})
```
然而 [tapable](https://github.com/webpack/tapable) 這個工具庫提供的鉤子類型遠不止這幾種,多樣化的鉤子類型,主要是為了能夠覆蓋多種使用場景:
* 連續地執行注冊的事件函數
* 并行地執行注冊的事件函數
* 一個接一個地執行注冊的事件函數,從前邊的事件函數獲取輸入,即瀑布流的方式
* 異步地執行注冊的事件函數
* 在允許時停止執行注冊的事件函數,一旦一個方法返回了一個非 `undefined` 的值,就跳出執行流
除了同步和異步的區別,我們再參考上述這一些使用場景,以及官方文檔的 [Plugin API](https://doc.webpack-china.org/api/plugins/#tapable-%E5%92%8C-tapable-%E5%AE%9E%E4%BE%8B),進一步將事件鉤子類型做一個區分。
名稱帶有 `parallel` 的,注冊的事件函數會并行調用,如:
* AsyncParallelHook
* AsyncParallelBailHook
名稱帶有 `bail` 的,注冊的事件函數會被順序調用,直至一個處理方法有返回值(ParallelBail 的事件函數則會并行調用,第一個返回值會被使用):
* SyncBailHook
* AsyncParallelBailHook
* AsyncSeriesBailHook
名稱帶有 `waterfall` 的,每個注冊的事件函數,會將上一個方法的返回結果作為輸入參數,如:
* SyncWaterfallHook
* AsyncSeriesWaterfallHook
通過上面的名稱可以看出,有一些類型是可以結合到一起的,如 `AsyncParallelBailHook`,這樣它就具備了更加多樣化的特性。
了解了 webpack 中使用的各個事件鉤子的類型,才能在開發 plugin 更好地去把握注冊事件的輸入和輸出,同步和異步,來更好地完成我們想要的構建需求。
> 關于 webpack 3.x 的 plugin API,現在還可以參考官方文檔,趁著還沒更新到 4.x 版本:[plugin API](https://doc.webpack-china.org/api/plugins/)。
## 小結
本小節我們介紹了一個簡單的 webpack plugin 是怎么樣的,以及如何去開發和調試 plugin。
webpack plugin 的實現本質上就是基于 webpack 的構建流程注冊各種各樣的鉤子事件函數來添加額外的構建功能,所以我們也介紹了 webpack 流程中的事件鉤子以及事件鉤子的類型和區別,以便我們更好地在開發 plugin 時把握輸入輸出。
## 例子
本小節提及的一些簡單的 Demo 可以在 [webpack-examples](https://github.com/teabyii/webpack-examples) 找到。