本文將帶你一塊兒開發你的第一個 Webpack 插件,從 Webpack 配置工程師,邁向 Webpack 開發工程師!作本身的輪子,讓別人用去吧。html
1、背景介紹本文靈感源自業務中的經驗總結,不怕神同樣的產品,只怕一根筋的開發。webpack
在項目打包遇到問題:「當項目託管到 CDN 平臺,但願實現項目中的 index.js 不被緩存」。由於咱們須要修改 index.js 中的內容,不想用戶被緩存。web
思考一陣,有這麼幾種思路:數組
(聰明的你還有其餘方法,歡迎討論)緩存
思路分析:架構
因而我準備使用第三種方式,在app
index.html
生成以前完成下面修改:異步
問題簡單,實際仍是想試試開發 Webpack Plugin。ide
2、基礎知識Webpack 使用階段式的構建回調,開發者能夠引入它們本身的行爲到 Webpack 構建流程中。在開發以前,須要瞭解如下 Webpack 相關概念:svg
在自定義插件以前,咱們須要瞭解,一個 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 })] }; 複製代碼
HtmlWebpackPlugin 簡化了 HTML 文件的建立,以便爲你的 Webpack 包提供服務。這對於在文件名中包含每次會隨着編譯而發生變化哈希的 webpack bundle 尤爲有用。
插件的基本做用歸納:生成 HTML 文件。
html-webapck-plugin 插件兩個主要做用:
html-webapck-plugin 插件原理介紹:
本文開發的 自動添加時間戳引用腳本文件(SetScriptTimestampPlugin) 插件實現的原理:經過 HtmlWebpackPlugin 生成 HTML 文件前,將模版文件預留位置替換成腳本,腳本中執行自動添加時間戳來引用腳本文件。
3.2 初始化插件文件
新建 SetScriptTimestampPlugin.js 文件,並參考官方文檔中插件的基本結構,初始化插件代碼:
// SetScriptTimestampPlugin.jsclass SetScriptTimestampPlugin { apply(compiler) { compiler.hooks.done.tap('SetScriptTimestampPlugin', (compilation, callback) => { console.log('SetScriptTimestampPlugin!'); }); } } module.exports = SetScriptTimestampPlugin; 複製代碼
apply 方法爲插件原型方法,接收 compiler 做爲參數。
選擇插件觸發時機,實際上是選擇插件觸發的 compiler 鉤子(即什麼時候觸發插件)。Webpack 提供鉤子有不少,這裏簡單介紹幾個,完整具體可參考文檔《Compiler Hooks》:
咱們插件應該是要在 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 提供三種觸發鉤子的方法:
這三種方式能選擇的鉤子方法也不一樣,因爲 compilation 是 SyncHook 同步鉤子,因此採用 tap 觸發方式。tap 方法接收兩個參數:插件名稱和回調函數。
咱們原理上是將模版文件中,指定替換入口,再替換成須要執行的腳本。
[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> 複製代碼
到這一步,纔開始編寫插件的邏輯。從上一步中,咱們知道在 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; 複製代碼
在上面插件邏輯中,具體作了這些事:
所謂「插件事件」即插件所提供的一些事件,用於監聽插件狀態,這裏列舉幾個 html-webpack-plugin 提供的事件(完整可查看《html-webpack-plugin》):Async:
Sync:
在回調方法中,經過 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>)
替換邏輯即:動態建立一個 script 標籤,將其 src 值設置爲上一步讀取到的腳本文件名,並在後面拼接 時間戳 做爲參數。
經過 htmlPluginData.html 能夠獲取到模版文件的字符串輸出,咱們只須要將模版字符串中替換入口 <!--SetScriptTimestampPlugin inset script--> 替換成咱們上一步編寫的替換邏輯便可。
最後將修改後的 HTML 字符串,賦值給原來的 htmlPluginData.html 達到修改效果。
自定義插件使用方式,與其餘插件一致,在 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 插件爲例子,繼續拓展。
每一個插件本質是一個類,跟一個類實例化相同,能夠在實例化時傳入配置參數,在構造函數中操做:
// 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" }) ] } 複製代碼
若是咱們此時須要同時修改多個腳本文件的時間戳,也只須要將參數類型和執行腳本作下調整。具體修改腳本,這裏不具體展開,篇幅有限,能夠自行思考實現咯~這裏展現使用插件時的參數:
// 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》進行學習。