Webpack 插件開發指南


本文將帶你一塊兒開發你的第一個 Webpack 插件,從 Webpack 配置工程師,邁向 Webpack 開發工程師!作本身的輪子,讓別人用去吧。html

1、背景介紹

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

在項目打包遇到問題:「當項目託管到 CDN 平臺,但願實現項目中的 index.js 不被緩存」。由於咱們須要修改 index.js 中的內容,不想用戶被緩存。web

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

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

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

思路分析:架構

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

因而我準備使用第三種方式,在app

index.html

生成以前完成下面修改:異步

問題簡單,實際仍是想試試開發 Webpack Plugin。ide

2、基礎知識

Webpack 使用階段式的構建回調,開發者能夠引入它們本身的行爲到 Webpack 構建流程中。在開發以前,須要瞭解如下 Webpack 相關概念:svg

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.jsvar 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 標籤引入腳本文件;
3、開發流程

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

3.1 插件運行機制

 

3.2 初始化插件文件

新建 SetScriptTimestampPlugin.js  文件,並參考官方文檔中插件的基本結構,初始化插件代碼:

// SetScriptTimestampPlugin.jsclass 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 添加插件替換入口

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

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="947" height="340"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="947" height="340"></svg>)

因此咱們在模版文件 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.jsclass 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 引入的腳本文件名稱列表,拷貝一份,並清空原有列表。

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="334" height="93"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="334" height="93"></svg>)

  1. 編寫替換邏輯。

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

  1. 插入替換邏輯。

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

  1. 返回HTML文件。

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

3.5 使用插件

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

// webpack.config.jsconst SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
	// ... 省略其餘配置plugins: [
  	// ... 省略其餘插件new SetScriptTimestampPlugin()  
  ]
}
複製代碼

到這一步,咱們已經實現需求「當項目託管到 CDN 平臺,但願實現項目中的 index.js 不被緩存」。

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="467" height="291"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="467" height="291"></svg>)

4、案例拓展

這裏以以前 SetScriptTimestampPlugin 插件爲例子,繼續拓展。

4.1 讀取插件配置參數

每一個插件本質是一個類,跟一個類實例化相同,能夠在實例化時傳入配置參數,在構造函數中操做:

// SetScriptTimestampPlugin.jsclass SetScriptTimestampPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    console.log(this.options.filename); // "index.js"// ... 省略其餘代碼
  }
}
module.exports = SetScriptTimestampPlugin;
複製代碼

使用時:

// webpack.config.jsconst SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
	// ... 省略其餘配置plugins: [
  	// ... 省略其餘插件new SetScriptTimestampPlugin({
    	filename: "index.js"
    })  
  ]
}
複製代碼

4.2 添加多腳本文件的時間戳

若是咱們此時須要同時修改多個腳本文件的時間戳,也只須要將參數類型和執行腳本作下調整。具體修改腳本,這裏不具體展開,篇幅有限,能夠自行思考實現咯~這裏展現使用插件時的參數:

// webpack.config.jsconst 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>
複製代碼

5、總結

本文通用自定義 Webpack 插件來實現平常一些比較棘手的需求。主要爲你們介紹了 Webpack 插件的基本組成和簡單架構,也介紹了 HtmlWebpackPlugin 插件。並經過這些基礎知識,完成了一個 HTML 文本替換插件,最後經過兩個場景來拓展插件使用範圍。

最後,關於 Webpack 插件開發,還有更多知識能夠學習,建議多看看官方文檔《Writing a Plugin》進行學習。

相關文章
相關標籤/搜索