本文使用的Webpack-Quickly-Starter快速搭建 Webpack4 本地學(xué)習(xí)環(huán)境。
建議多閱讀 Webpack 文檔《Writing a Plugin》章節(jié)膏执,學(xué)習(xí)開發(fā)簡單插件棒旗。
本文將帶你一起開發(fā)你的第一個(gè) Webpack 插件,從 Webpack 配置工程師朋贬,邁向 Webpack 開發(fā)工程師!
做自己的輪子窜骄,讓別人用去吧锦募。
完整代碼存放在:https://github.com/pingan8787/script-timestamp-webpack-plugin
一、背景介紹
本文靈感源自業(yè)務(wù)中的經(jīng)驗(yàn)總結(jié)邻遏,不怕神一樣的產(chǎn)品糠亩,只怕一根筋的開發(fā)。
在項(xiàng)目打包遇到問題:“當(dāng)項(xiàng)目托管到 CDN 平臺(tái)准验,希望實(shí)現(xiàn)項(xiàng)目中的 index.js 不被緩存”赎线。因?yàn)槲覀冃枰薷?index.js
中的內(nèi)容,不想用戶被緩存糊饱。
思考一陣垂寥,有這么幾種思路:
- 在 CDN 平臺(tái)中過濾該文件的緩存設(shè)置;
- 查找 DOM 元素,修改該
script
標(biāo)簽的src
值滞项,并添加時(shí)時(shí)間戳狭归; - 打包時(shí)動(dòng)態(tài)創(chuàng)建
script
標(biāo)簽引入文件,并添加時(shí)時(shí)間戳文判。
(聰明的你還有其他方法过椎,歡迎討論)
思路分析:
- 顯然修改 CDN 設(shè)置的話,治標(biāo)不治本戏仓;
- 在模版文件中疚宇,添加
script
標(biāo)簽,執(zhí)行獲取 Webpack 自動(dòng)添加的script
標(biāo)簽并為其src
值添加時(shí)間戳柜去。但事實(shí)是還沒等你修改完灰嫉, js 文件已經(jīng)加載完畢,所以放棄 - 需要在
index.html
生成之前嗓奢,修改 js 文件的路徑讼撒,并添加時(shí)間戳。
于是我準(zhǔn)備使用第三種方式股耽,在 index.html
生成之前完成下面修改:
問題簡單根盒,實(shí)際還是想試試開發(fā) Webpack Plugin。
二物蝙、基礎(chǔ)知識(shí)
Webpack 使用階段式的構(gòu)建回調(diào)炎滞,開發(fā)者可以引入它們自己的行為到 Webpack 構(gòu)建流程中。
在開發(fā)之前诬乞,需要了解以下 Webpack 相關(guān)概念:
2.1 Webpack 插件組成
在自定義插件之前册赛,我們需要了解,一個(gè) Webpack 插件由哪些構(gòu)成震嫉,下面摘抄文檔:
- 一個(gè)具名 JavaScript 函數(shù)森瘪;
- 在它的原型上定義 apply 方法;
- 指定一個(gè)觸及到 Webpack 本身的事件鉤子票堵;
- 操作 Webpack 內(nèi)部的實(shí)例特定數(shù)據(jù)扼睬;
- 在實(shí)現(xiàn)功能后調(diào)用 Webpack 提供的 callback。
2.2 Webpack 插件基本架構(gòu)
插件由一個(gè)構(gòu)造函數(shù)實(shí)例化出來悴势。構(gòu)造函數(shù)定義 apply
方法窗宇,在安裝插件時(shí),apply
方法會(huì)被 Webpack compiler
調(diào)用一次特纤。apply
方法可以接收一個(gè) Webpack compiler
對(duì)象的引用军俊,從而可以在回調(diào)函數(shù)中訪問到 compiler
對(duì)象。
官方文檔提供一個(gè)簡單的插件結(jié)構(gòu):
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap('Hello World Plugin', (
stats /* 在 hook 被觸及時(shí)捧存,會(huì)將 stats 作為參數(shù)傳入蝇完。 */
) => {
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 文件的創(chuàng)建官硝,以便為你的 Webpack 包提供服務(wù)。這對(duì)于在文件名中包含每次會(huì)隨著編譯而發(fā)生變化哈希的 webpack bundle 尤其有用短蜕。
插件的基本作用概括:生成 HTML 文件。
html-webapck-plugin
插件兩個(gè)主要作用:
- 為 HTML 文件引入外部資源(如
script
/link
)動(dòng)態(tài)添加每次編譯后的 hash傻咖,防止引用文件的緩存問題朋魔; - 動(dòng)態(tài)創(chuàng)建 HTML 入口文件,如單頁應(yīng)用的
index.html
文件卿操。
html-webapck-plugin
插件原理介紹:
- 讀取 Webpack 中
entry
配置的相關(guān)入口chunk
和extract-text-webpack-plugin
插件抽取的 CSS 樣式警检; - 將樣式插入到插件提供的
template
或templateContent
配置指定的模版文件中; - 插入方式是:通過
link
標(biāo)簽引入樣式害淤,通過script
標(biāo)簽引入腳本文件扇雕;
三、開發(fā)流程
本文開發(fā)的 自動(dòng)添加時(shí)間戳引用腳本文件(SetScriptTimestampPlugin) 插件實(shí)現(xiàn)的原理:通過 HtmlWebpackPlugin 生成 HTML 文件前窥摄,將模版文件預(yù)留位置替換成腳本镶奉,腳本中執(zhí)行自動(dòng)添加時(shí)間戳來引用腳本文件。
3.1 插件運(yùn)行機(jī)制
3.2 初始化插件文件
新建 SetScriptTimestampPlugin.js
文件崭放,并參考官方文檔中插件的基本結(jié)構(gòu)哨苛,初始化插件代碼:
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
apply(compiler) {
compiler.hooks.done.tap('SetScriptTimestampPlugin',
(compilation, callback) => {
console.log('SetScriptTimestampPlugin!');
});
}
}
module.exports = SetScriptTimestampPlugin;
apply
方法為插件原型方法,接收 compiler
作為參數(shù)币砂。
3.3 選擇插件觸發(fā)時(shí)機(jī)
選擇插件觸發(fā)時(shí)機(jī)建峭,其實(shí)是選擇插件觸發(fā)的 compiler 鉤子(即何時(shí)觸發(fā)插件)。
Webpack 提供鉤子有很多决摧,這里簡單介紹幾個(gè)亿蒸,完整具體可參考文檔《Compiler Hooks》:
-
entryOption
: 在 webpack 選項(xiàng)中的entry
配置項(xiàng) 處理過之后,執(zhí)行插件掌桩。 -
afterPlugins
: 設(shè)置完初始插件之后边锁,執(zhí)行插件。 -
compilation
: 編譯創(chuàng)建之后拘鞋,生成文件之前砚蓬,執(zhí)行插件。盆色。 -
emit
: 生成資源到output
目錄之前灰蛙。 -
done
: 編譯完成。
我們插件應(yīng)該是要在 HTML 輸出之前隔躲,動(dòng)態(tài)添加 script
標(biāo)簽摩梧,所以我們選擇鉤入 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
下指定事件鉤子函數(shù)宣旱,便會(huì)觸發(fā)鉤子時(shí)仅父,執(zhí)行回調(diào)函數(shù)。
Webpack 提供三種觸發(fā)鉤子的方法:
-
tap
:以同步方式觸發(fā)鉤子; -
tapAsync
:以異步方式觸發(fā)鉤子笙纤; -
tapPromise
:以異步方式觸發(fā)鉤子耗溜,返回 Promise;
這三種方式能選擇的鉤子方法也不同省容,由于 compilation
是 SyncHook
同步鉤子抖拴,所以采用 tap
觸發(fā)方式。
tap
方法接收兩個(gè)參數(shù):插件名稱和回調(diào)函數(shù)腥椒。
3.4 添加插件替換入口
我們?cè)砩鲜菍⒛0嫖募邪⒄付ㄌ鎿Q入口,再替換成需要執(zhí)行的腳本笼蛛。
所以我們?cè)谀0嫖募?
template.html
中添加 SetScriptTimestampPlugininsetscript
作為標(biāo)識(shí)替換入口:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack 插件開發(fā)入門</title>
</head>
<body>
<!-- other code -->
<!--SetScriptTimestampPlugin inset script-->
</body>
</html>
3.5 編寫插件邏輯
到這一步洒放,才開始編寫插件的邏輯。
從上一步中滨砍,我們知道在 tap
第二個(gè)參數(shù)是個(gè)回調(diào)函數(shù)往湿,并且這個(gè)回調(diào)函數(shù)有兩個(gè)參數(shù): compilation
和 callback
。
compilation
繼承于compiler
惨好,包含 compiler
所有內(nèi)容(也有 Webpack 的 options
)煌茴,而且也有 plugin
函數(shù)接入任務(wù)點(diǎn)。
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('SetScriptTimestampPlugin',
(compilation, callback) => {
// 插件邏輯 調(diào)用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
);
// 返回修改后的結(jié)果
htmlPluginData.html = resultHTML;
}
);
}
);
}
}
module.exports = SetScriptTimestampPlugin;
在上面插件邏輯中日川,具體做了這些事:
- 執(zhí)行
compilation.plugin
方法蔓腐,并傳入兩個(gè)參數(shù):插件事件和回調(diào)方法。
所謂“插件事件”即插件所提供的一些事件龄句,用于監(jiān)聽插件狀態(tài)回论,這里列舉幾個(gè) 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
- 獲取腳本文件名稱列表并清空。
在回調(diào)方法中分歇,通過 htmlPluginData.assets.js
獲取需要通過 script
引入的腳本文件名稱列表傀蓉,拷貝一份,并清空原有列表职抡。
- 編寫替換邏輯葬燎。
替換邏輯即:動(dòng)態(tài)創(chuàng)建一個(gè) script
標(biāo)簽,將其 src
值設(shè)置為上一步讀取到的腳本文件名缚甩,并在后面拼接 時(shí)間戳 作為參數(shù)谱净。
- 插入替換邏輯。
通過 htmlPluginData.html
可以獲取到模版文件的字符串輸出擅威,我們只需要將模版字符串中替換入口 `` 替換成我們上一步編寫的替換邏輯即可壕探。
- 返回HTML文件。
最后將修改后的 HTML 字符串郊丛,賦值給原來的 htmlPluginData.html
達(dá)到修改效果李请。
3.5 使用插件
自定義插件使用方式瞧筛,與其他插件一致,在 plugins
數(shù)組中實(shí)例化:
// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin()
]
}
到這一步导盅,我們已經(jīng)實(shí)現(xiàn)需求“當(dāng)項(xiàng)目托管到 CDN 平臺(tái)较幌,希望實(shí)現(xiàn)項(xiàng)目中的 index.js 不被緩存”。
四认轨、案例拓展
這里以之前 SetScriptTimestampPlugin 插件為例子绅络,繼續(xù)拓展。
4.1 讀取插件配置參數(shù)
每個(gè)插件本質(zhì)是一個(gè)類嘁字,跟一個(gè)類實(shí)例化相同,可以在實(shí)例化時(shí)傳入配置參數(shù)杉畜,在構(gòu)造函數(shù)中操作:
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
console.log(this.options.filename); // "index.js"
// ... 省略其他代碼
}
}
module.exports = SetScriptTimestampPlugin;
使用時(shí):
// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin({
filename: "index.js"
})
]
}
4.2 添加多腳本文件的時(shí)間戳
如果我們此時(shí)需要同時(shí)修改多個(gè)腳本文件的時(shí)間戳纪蜒,也只需要將參數(shù)類型和執(zhí)行腳本做下調(diào)整。
具體修改腳本此叠,這里不具體展開纯续,篇幅有限,可以自行思考實(shí)現(xiàn)咯~
這里展示使用插件時(shí)的參數(shù):
// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin({
filename: ["index.js", "boundle.js", "pingan.js"]
})
]
}
生成結(jié)果:
<script src="./index.js?1582425467655"></script>
<script src="./boundle.js?1582425467655"></script>
<script src="./pingan.js?1582425467655"></script>
五灭袁、總結(jié)
本文通用自定義 Webpack 插件來實(shí)現(xiàn)日常一些比較棘手的需求猬错。主要為大家介紹了 Webpack 插件的基本組成和簡單架構(gòu),也介紹了 HtmlWebpackPlugin 插件茸歧。并通過這些基礎(chǔ)知識(shí)倦炒,完成了一個(gè) HTML 文本替換插件,最后通過兩個(gè)場景來拓展插件使用范圍软瞎。
最后逢唤,關(guān)于 Webpack 插件開發(fā),還有更多知識(shí)可以學(xué)習(xí)涤浇,建議多看看官方文檔《Writing a Plugin》進(jìn)行學(xué)習(xí)鳖藕。
本文純屬個(gè)人經(jīng)驗(yàn)總結(jié),如有異議只锭,歡迎指點(diǎn)著恩。