[轉(zhuǎn)]【W(wǎng)ebpack】??Webpack 插件開發(fā)如此簡單!

本文使用的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

image

一、背景介紹

本文靈感源自業(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)容,不想用戶被緩存糊饱。

思考一陣垂寥,有這么幾種思路:

  1. 在 CDN 平臺(tái)中過濾該文件的緩存設(shè)置;
  2. 查找 DOM 元素,修改該 script 標(biāo)簽的 src 值滞项,并添加時(shí)時(shí)間戳狭归;
  3. 打包時(shí)動(dòng)態(tài)創(chuàng)建 script 標(biāo)簽引入文件,并添加時(shí)時(shí)間戳文判。

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

思路分析:

  1. 顯然修改 CDN 設(shè)置的話,治標(biāo)不治本戏仓;
  2. 在模版文件中疚宇,添加 script 標(biāo)簽,執(zhí)行獲取 Webpack 自動(dòng)添加的 script 標(biāo)簽并為其 src 值添加時(shí)間戳柜去。但事實(shí)是還沒等你修改完灰嫉, js 文件已經(jīng)加載完畢,所以放棄
  3. 需要在 index.html 生成之前嗓奢,修改 js 文件的路徑讼撒,并添加時(shí)間戳。

于是我準(zhǔn)備使用第三種方式股耽,在 index.html 生成之前完成下面修改:

image.png

問題簡單根盒,實(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)入口 chunkextract-text-webpack-plugin 插件抽取的 CSS 樣式警检;
  • 將樣式插入到插件提供的 templatetemplateContent 配置指定的模版文件中;
  • 插入方式是:通過 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ī)制

image.png

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;

這三種方式能選擇的鉤子方法也不同省容,由于 compilationSyncHook 同步鉤子抖拴,所以采用 tap 觸發(fā)方式。
tap 方法接收兩個(gè)參數(shù):插件名稱和回調(diào)函數(shù)腥椒。

3.4 添加插件替換入口

我們?cè)砩鲜菍⒛0嫖募邪⒄付ㄌ鎿Q入口,再替換成需要執(zhí)行的腳本笼蛛。

image.png

所以我們?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ù): compilationcallback

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;

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

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

在回調(diào)方法中分歇,通過 htmlPluginData.assets.js 獲取需要通過 script 引入的腳本文件名稱列表傀蓉,拷貝一份,并清空原有列表职抡。

image.png

  1. 編寫替換邏輯葬燎。

替換邏輯即:動(dòng)態(tài)創(chuàng)建一個(gè) script 標(biāo)簽,將其 src 值設(shè)置為上一步讀取到的腳本文件名缚甩,并在后面拼接 時(shí)間戳 作為參數(shù)谱净。

  1. 插入替換邏輯。

通過 htmlPluginData.html 可以獲取到模版文件的字符串輸出擅威,我們只需要將模版字符串中替換入口 `` 替換成我們上一步編寫的替換邏輯即可壕探。

  1. 返回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 不被緩存”。


image.png

四认轨、案例拓展

這里以之前 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)著恩。

參考文檔

  1. Writing a Plugin
  2. HtmlWebpackPlugin - Webpack
  3. 擴(kuò)展 HtmlwebpackPlugin 插入自定義的腳本
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜻展,隨后出現(xiàn)的幾起案子喉誊,更是在濱河造成了極大的恐慌,老刑警劉巖铺呵,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裹驰,死亡現(xiàn)場離奇詭異,居然都是意外死亡片挂,警方通過查閱死者的電腦和手機(jī)幻林,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門贞盯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沪饺,你說我怎么就攤上這事躏敢。” “怎么了整葡?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵件余,是天一觀的道長。 經(jīng)常有香客問我遭居,道長啼器,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任俱萍,我火速辦了婚禮端壳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘枪蘑。我一直安慰自己损谦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布岳颇。 她就那樣靜靜地躺著照捡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪话侧。 梳的紋絲不亂的頭發(fā)上栗精,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音掂摔,去河邊找鬼术羔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛乙漓,可吹牛的內(nèi)容都是我干的级历。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼叭披,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼寥殖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涩蜘,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤嚼贡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后同诫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粤策,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年误窖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叮盘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秩贰。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖柔吼,靈堂內(nèi)的尸體忽然破棺而出毒费,到底是詐尸還是另有隱情,我是刑警寧澤愈魏,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布觅玻,位于F島的核電站,受9級(jí)特大地震影響培漏,放射性物質(zhì)發(fā)生泄漏溪厘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一牌柄、第九天 我趴在偏房一處隱蔽的房頂上張望桩匪。 院中可真熱鬧,春花似錦友鼻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至僻爽,卻和暖如春虫碉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背胸梆。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工敦捧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碰镜。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓兢卵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绪颖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子秽荤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容