🔥Webpack插件開發如此簡單!

NO IMAGE

本文使用的Webpack-Quickly-Starter快速搭建 Webpack4 本地學習環境。
建議多閱讀 Webpack 文檔《Writing a Plugin》章節,學習開發簡單插件。

本文將帶你一起開發你的第一個 Webpack 插件,從 Webpack 配置工程師,邁向 Webpack 開發工程師!
做自己的輪子,讓別人用去吧。

完整代碼存放在:github.com/pingan8787/…

🔥Webpack插件開發如此簡單!

一、背景介紹

本文靈感源自業務中的經驗總結,不怕神一樣的產品,只怕一根筋的開發

在項目打包遇到問題:“當項目託管到 CDN 平臺,希望實現項目中的 index.js 不被緩存”。因為我們需要修改 index.js 中的內容,不想用戶被緩存。

思考一陣,有這麼幾種思路:

  1. 在 CDN 平臺中過濾該文件的緩存設置;
  2. 查找 DOM 元素,修改該 script 標籤的 src 值,並添加時時間戳;
  3. 打包時動態創建 script 標籤引入文件,並添加時時間戳。

(聰明的你還有其他方法,歡迎討論)

思路分析:

  1. 顯然修改 CDN 設置的話,治標不治本;
  2. 在模版文件中,添加 script 標籤,執行獲取 Webpack 自動添加的 script 標籤併為其 src 值添加時間戳。但事實是還沒等你修改完, js 文件已經加載完畢,所以放棄
  3. 需要在 index.html 生成之前,修改 js 文件的路徑,並添加時間戳。

於是我準備使用第三種方式,在 index.html 生成之前完成下面修改:

🔥Webpack插件開發如此簡單!

問題簡單,實際還是想試試開發 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 配置的相關入口 chunkextract-text-webpack-plugin 插件抽取的 CSS 樣式;
  • 將樣式插入到插件提供的 templatetemplateContent 配置指定的模版文件中;
  • 插入方式是:通過 link 標籤引入樣式,通過 script 標籤引入腳本文件;

三、開發流程

本文開發的 自動添加時間戳引用腳本文件(SetScriptTimestampPlugin) 插件實現的原理:通過 HtmlWebpackPlugin 生成 HTML 文件前,將模版文件預留位置替換成腳本,腳本中執行自動添加時間戳來引用腳本文件。

3.1 插件運行機制

🔥Webpack插件開發如此簡單!

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》:

  • 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 添加插件替換入口

我們原理上是將模版文件中,指定替換入口,再替換成需要執行的腳本。

🔥Webpack插件開發如此簡單!

所以我們在模版文件 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;

在上面插件邏輯中,具體做了這些事:

  1. 執行 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
  1. 獲取腳本文件名稱列表並清空。

在回調方法中,通過 htmlPluginData.assets.js 獲取需要通過 script 引入的腳本文件名稱列表,拷貝一份,並清空原有列表。

🔥Webpack插件開發如此簡單!

  1. 編寫替換邏輯。

替換邏輯即:動態創建一個 script 標籤,將其 src 值設置為上一步讀取到的腳本文件名,並在後面拼接 時間戳 作為參數。

  1. 插入替換邏輯。

通過 htmlPluginData.html 可以獲取到模版文件的字符串輸出,我們只需要將模版字符串中替換入口 <!--SetScriptTimestampPlugin inset script--> 替換成我們上一步編寫的替換邏輯即可。

  1. 返回HTML文件。

最後將修改後的 HTML 字符串,賦值給原來的 htmlPluginData.html 達到修改效果。

3.5 使用插件

自定義插件使用方式,與其他插件一致,在 plugins 數組中實例化:

// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin()  
]
}

到這一步,我們已經實現需求“當項目託管到 CDN 平臺,希望實現項目中的 index.js 不被緩存”。

🔥Webpack插件開發如此簡單!

四、案例拓展

這裡以之前 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》進行學習。

本文純屬個人經驗總結,如有異議,歡迎指點。

參考文檔

  1. Writing a Plugin
  2. HtmlWebpackPlugin – Webpack
  3. 擴展 HtmlwebpackPlugin 插入自定義的腳本

關於我

Author王平安
E-mail[email protected]
博 客www.pingan8787.com
微 信pingan8787
每日文章推薦github.com/pingan8787/…
ES小冊js.pingan8787.com

微信公眾號

🔥Webpack插件開發如此簡單!

相關文章

Java併發原理抽絲剝繭,讀寫鎖ReadWriteLock實現深入剖析

SpringBoot要怎麼學?要學哪些東西?要不要先學SSM?鬆哥說說看法

前端代碼是怎樣智能生成的業務邏輯智能生成篇

從零手寫一套Express的源碼