【Webpack】513- Webpack 插件開發如此簡單!
- 2020 年 2 月 26 日
- 筆記
本文使用的Webpack-Quickly-Starter快速搭建 Webpack4 本地學習環境。 建議多閱讀 Webpack 文檔《Writing a Plugin》章節,學習開發簡單插件。 」
本文將帶你一起開發第一個 Webpack 插件,從 Webpack 配置工程師,邁向 Webpack 開發工程師! 做自己的輪子,讓別人用去吧。
完整程式碼:https://github.com/pingan8787/script-timestamp-webpack-plugin

一、背景介紹
本文靈感源自業務中的經驗總結,不怕神一樣的產品,只怕一根筋的開發。
在項目打包遇到問題:「當項目託管到 CDN 平台,希望實現項目中的 index.js 不被快取」。因為我們需要修改 index.js 中的內容,不想用戶被快取。
思考一陣,有這麼幾種思路:
- 在 CDN 平台中過濾該文件的快取設置;
- 查找 DOM 元素,修改該
script標籤的src值,並添加時時間戳; - 打包時動態創建
script標籤引入文件,並添加時時間戳。
(聰明的你還有其他方法,歡迎討論)
思路分析:
- 顯然修改 CDN 設置的話,治標不治本;
- 在模版文件中,添加
script標籤,執行獲取 Webpack 自動添加的script標籤並為其src值添加時間戳。但事實是還沒等你修改完, js 文件已經載入完畢,所以放棄 - 需要在
index.html生成之前,修改 js 文件的路徑,並添加時間戳。
於是我準備使用第三種方式,在 index.html 生成之前完成下面修改:

問題簡單,實際還是想試試開發 Webpack Plugin。
二、基礎知識
Webpack 使用階段式的構建回調,開發者可以引入它們自己的行為到 Webpack 構建流程中。 在開發之前,需要了解以下 Webpack 相關概念:
2.1 Webpack 插件組成
在自定義插件之前,我們需要了解,一個 Webpack 插件由哪些構成,下面摘抄文檔:
- 一個具名 JavaScript 函數;
- 在它的原型上定義 apply 方法;
- 指定一個觸及到 Webpack 本身的事件鉤子;
- 操作 Webpack 內部的實例特定數據;
- 在實現功能後調用 Webpack 提供的 callback。
2.2 Webpack 插件基本架構
插件由一個構造函數實例化出來。構造函數定義 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.config.js var HelloWorldPlugin = require('hello-world'); module.exports = { // ... 這裡是其他配置 ... plugins: [new HelloWorldPlugin({ options: true })] }; 2.3 HtmlWebpackPlugin 介紹
HtmlWebpackPlugin 簡化了 HTML 文件的創建,以便為你的 Webpack 包提供服務。這對於在文件名中包含每次會隨著編譯而發生變化哈希的 webpack bundle 尤其有用。 」
插件的基本作用概括:生成 HTML 文件。
html-webapck-plugin 插件兩個主要作用:
- 為 HTML 文件引入外部資源(如
script/link)動態添加每次編譯後的 hash,防止引用文件的快取問題; - 動態創建 HTML 入口文件,如單頁應用的
index.html文件。
html-webapck-plugin 插件原理介紹:
- 讀取 Webpack 中
entry配置的相關入口chunk和extract-text-webpack-plugin插件抽取的 CSS 樣式; - 將樣式插入到插件提供的
template或templateContent配置指定的模版文件中; - 插入方式是:通過
link標籤引入樣式,通過script標籤引入腳本文件;
三、開發流程
本文開發的 自動添加時間戳引用腳本文件(SetScriptTimestampPlugin) 插件實現的原理:通過 HtmlWebpackPlugin 生成 HTML 文件前,將模版文件預留位置替換成腳本,腳本中執行自動添加時間戳來引用腳本文件。
3.1 插件運行機制

SetScriptTimestampPlugin 運行機制.png
3.2 初始化插件文件
新建 SetScriptTimestampPlugin.js 文件,並參考官方文檔中插件的基本結構,初始化插件程式碼:
// SetScriptTimestampPlugin.js class SetScriptTimestampPlugin { apply(compiler) { compiler.hooks.done.tap('SetScriptTimestampPlugin', (compilation, callback) => { console.log('SetScriptTimestampPlugin!'); }); } } module.exports = SetScriptTimestampPlugin; apply 方法為插件原型方法,接收 compiler 作為參數。
3.3 選擇插件觸發時機
選擇插件觸發時機,其實是選擇插件觸發的 compiler 鉤子(即何時觸發插件)。 Webpack 提供鉤子有很多,這裡簡單介紹幾個,完整具體可參考文檔《Compiler Hooks》:文檔地址:https://webpack.js.org/api/compiler-hooks/
entryOption: 在 webpack 選項中的entry配置項 處理過之後,執行插件。afterPlugins: 設置完初始插件之後,執行插件。compilation: 編譯創建之後,生成文件之前,執行插件。。emit: 生成資源到output目錄之前。done: 編譯完成。
我們插件應該是要在 HTML 輸出之前,動態添加 script 標籤,所以我們選擇鉤入 compilation 階段,程式碼修改:
// SetScriptTimestampPlugin.js class SetScriptTimestampPlugin { apply(compiler) { - compiler.hooks.done.tap('SetScriptTimestampPlugin', + compiler.hooks.compilation.tap('SetScriptTimestampPlugin', (compilation, callback) => { console.log('SetScriptTimestampPlugin!'); }); } } module.exports = SetScriptTimestampPlugin; 在 compiler.hooks 下指定事件鉤子函數,便會觸發鉤子時,執行回調函數。 Webpack 提供三種觸發鉤子的方法:
tap:以同步方式觸發鉤子;tapAsync:以非同步方式觸發鉤子;tapPromise:以非同步方式觸發鉤子,返回 Promise;
這三種方式能選擇的鉤子方法也不同,由於 compilation 是 SyncHook 同步鉤子,所以採用 tap 觸發方式。 tap 方法接收兩個參數:插件名稱和回調函數。
3.4 添加插件替換入口
我們原理上是將模版文件中,指定替換入口,再替換成需要執行的腳本。
image.png
所以我們在模版文件 template.html 中添加 <!--SetScriptTimestampPlugin inset script--> 作為標識替換入口:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Webpack 插件開發入門</title> </head> <body> <!-- other code --> <!--SetScriptTimestampPlugin inset script--> </body> </html>
3.5 編寫插件邏輯
到這一步,才開始編寫插件的邏輯。 從上一步中,我們知道在 tap 第二個參數是個回調函數,並且這個回調函數有兩個參數:compilation 和 callback 。
compilation 繼承於compiler,包含 compiler 所有內容(也有 Webpack 的 options),而且也有 plugin 函數接入任務點。
// SetScriptTimestampPlugin.js class SetScriptTimestampPlugin { apply(compiler) { compiler.hooks.compilation.tap('SetScriptTimestampPlugin', (compilation, callback) => { // 插件邏輯 調用compilation提供的plugin方法 compilation.plugin( "html-webpack-plugin-before-html-processing", function(htmlPluginData, callback) { // 讀取並修改 script 上 src 列表 let jsScr = htmlPluginData.assets.js[0]; htmlPluginData.assets.js = []; let result = ` <script> let scriptDOM = document.createElement("script"); let jsScr = "./${jsScr}"; scriptDOM.src = jsScr + "?" + new Date().getTime(); document.body.appendChild(scriptDOM) </script> `; let resultHTML = htmlPluginData.html.replace( "<!--SetScriptTimestampPlugin inset script-->", result ); // 返回修改後的結果 htmlPluginData.html = resultHTML; } ); } ); } } module.exports = SetScriptTimestampPlugin; 在上面插件邏輯中,具體做了這些事:
- 執行
compilation.plugin方法,並傳入兩個參數:插件事件和回調方法。
所謂「插件事件」即插件所提供的一些事件,用於監聽插件狀態,這裡列舉幾個 html-webpack-plugin 提供的事件(完整可查看《html-webpack-plugin 官網》): Async:
html-webpack-plugin-before-html-generationhtml-webpack-plugin-before-html-processinghtml-webpack-plugin-alter-asset-tags
Sync:
html-webpack-plugin-alter-chunks
- 獲取腳本文件名稱列表並清空。
在回調方法中,通過 htmlPluginData.assets.js 獲取需要通過 script 引入的腳本文件名稱列表,拷貝一份,並清空原有列表。

- 編寫替換邏輯。
替換邏輯即:動態創建一個 script 標籤,將其 src 值設置為上一步讀取到的腳本文件名,並在後面拼接 時間戳 作為參數。
- 插入替換邏輯。
通過 htmlPluginData.html 可以獲取到模版文件的字元串輸出,我們只需要將模版字元串中替換入口 <!--SetScriptTimestampPlugin inset script--> 替換成我們上一步編寫的替換邏輯即可。
- 返回HTML文件。
最後將修改後的 HTML 字元串,賦值給原來的 htmlPluginData.html 達到修改效果。
3.5 使用插件
自定義插件使用方式,與其他插件一致,在 plugins 數組中實例化:
// webpack.config.js const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); module.exports = { // ... 省略其他配置 plugins: [ // ... 省略其他插件 new SetScriptTimestampPlugin() ] } 到這一步,我們已經實現需求「當項目託管到 CDN 平台,希望實現項目中的 index.js 不被快取」。
四、案例拓展
這裡以之前 SetScriptTimestampPlugin 插件為例子,繼續拓展。
4.1 讀取插件配置參數
每個插件本質是一個類,跟一個類實例化相同,可以在實例化時傳入配置參數,在構造函數中操作:
// SetScriptTimestampPlugin.js class SetScriptTimestampPlugin { constructor(options) { this.options = options; } apply(compiler) { console.log(this.options.filename); // "index.js" // ... 省略其他程式碼 } } module.exports = SetScriptTimestampPlugin; 使用時:
// webpack.config.js const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); module.exports = { // ... 省略其他配置 plugins: [ // ... 省略其他插件 new SetScriptTimestampPlugin({ filename: "index.js" }) ] } 4.2 添加多腳本文件的時間戳
如果我們此時需要同時修改多個腳本文件的時間戳,也只需要將參數類型和執行腳本做下調整。 具體修改腳本,這裡不具體展開,篇幅有限,可以自行思考實現咯~ 這裡展示使用插件時的參數:
// webpack.config.js const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); module.exports = { // ... 省略其他配置 plugins: [ // ... 省略其他插件 new SetScriptTimestampPlugin({ filename: ["index.js", "boundle.js", "pingan.js"] }) ] } 生成結果:
<script src="./index.js?1582425467655"></script> <script src="./boundle.js?1582425467655"></script> <script src="./pingan.js?1582425467655"></script>
五、總結
本文通用自定義 Webpack 插件來實現日常一些比較棘手的需求。主要為大家介紹了 Webpack 插件的基本組成和簡單架構,也介紹了 HtmlWebpackPlugin 插件。並通過這些基礎知識,完成了一個 HTML 文本替換插件,最後通過兩個場景來拓展插件使用範圍。
最後,關於 Webpack 插件開發,還有更多知識可以學習,建議多看看官方文檔《Writing a Plugin》進行學習。
本文屬個人經驗總結,如有異議,歡迎指點。
參考文檔
- 《Writing a Plugin》https://webpack.js.org/contribute/writing-a-plugin
- 《HtmlWebpackPlugin - Webpack》https://webpack.js.org/plugins/html-webpack-plugin
- 《擴展 HtmlwebpackPlugin 插入自定義的腳本》https://www.cnblogs.com/mjian/p/9250095.html
- 《Webpack-Quickly-Starter》 https://github.com/pingan8787/Webpack-Quickly-Starter

