【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-generation
html-webpack-plugin-before-html-processing
html-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