【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 中的內容,不想用戶被緩存。

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

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

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

思路分析:

  1. 顯然修改 CDN 設置的話,治標不治本;
  2. 在模版文件中,添加 script 標籤,執行獲取 Webpack 自動添加的 script 標籤並為其 src 值添加時間戳。但事實是還沒等你修改完, js 文件已經加載完畢,所以放棄
  3. 需要在 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 配置的相關入口 chunkextract-text-webpack-plugin 插件抽取的 CSS 樣式;
  • 將樣式插入到插件提供的 templatetemplateContent 配置指定的模版文件中;
  • 插入方式是:通過 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;

這三種方式能選擇的鉤子方法也不同,由於 compilationSyncHook 同步鉤子,所以採用 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 第二個參數是個回調函數,並且這個回調函數有兩個參數:compilationcallback

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 引入的腳本文件名稱列表,拷貝一份,並清空原有列表。

  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 不被緩存」。

四、案例拓展

這裡以之前 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》https://webpack.js.org/contribute/writing-a-plugin
  2. 《HtmlWebpackPlugin – Webpack》https://webpack.js.org/plugins/html-webpack-plugin
  3. 《擴展 HtmlwebpackPlugin 插入自定義的腳本》https://www.cnblogs.com/mjian/p/9250095.html
  4. 《Webpack-Quickly-Starter》 https://github.com/pingan8787/Webpack-Quickly-Starter