Webpack 經過 Plugin 機制讓其更加靈活,以適應各類應用場景。 在 Webpack 運行的生命週期中會廣播出許多事件,Plugin 能夠監聽這些事件,在合適的時機經過 Webpack 提供的 API 改變輸出結果。css
一個最基礎的 Plugin 的代碼是這樣的:html
class BasicPlugin{ // 在構造函數中獲取用戶給該插件傳入的配置 constructor(options){ } // Webpack 會調用 BasicPlugin 實例的 apply 方法給插件實例傳入 compiler 對象 apply(compiler){ compiler.plugin('compilation',function(compilation) { }) } } // 導出 Plugin module.exports = BasicPlugin; 複製代碼
在使用這個 Plugin 時,相關配置代碼以下:webpack
const BasicPlugin = require('./BasicPlugin.js'); module.export = { plugins:[ new BasicPlugin(options), ] } 複製代碼
Webpack 啓動後,在讀取配置的過程當中會先執行 new BasicPlugin(options)
初始化一個 BasicPlugin 得到其實例。 在初始化 compiler 對象後,再調用 basicPlugin.apply(compiler)
給插件實例傳入 compiler 對象。 插件實例在獲取到 compiler 對象後,就能夠經過 compiler.plugin(事件名稱, 回調函數)
監聽到 Webpack 廣播出來的事件。 而且能夠經過 compiler 對象去操做 Webpack。git
經過以上最簡單的 Plugin 相信你大概明白了 Plugin 的工做原理,但實際開發中還有不少細節須要注意,下面來詳細介紹。github
在開發 Plugin 時最經常使用的兩個對象就是 Compiler 和 Compilation,它們是 Plugin 和 Webpack 之間的橋樑。 Compiler 和 Compilation 的含義以下:web
Compiler 和 Compilation 的區別在於:Compiler 表明了整個 Webpack 從啓動到關閉的生命週期,而 Compilation 只是表明了一次新的編譯。數組
Webpack 就像一條生產線,要通過一系列處理流程後才能將源文件轉換成輸出結果。 這條生產線上的每一個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。 插件就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源作處理。服務器
Webpack 經過 Tapable 來組織這條複雜的生產線。 Webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運做。 Webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。markdown
Webpack 的事件流機制應用了觀察者模式,和 Node.js 中的 EventEmitter 很是類似。 Compiler 和 Compilation 都繼承自 Tapable,能夠直接在 Compiler 和 Compilation 對象上廣播和監聽事件,方法以下:app
/** * 廣播出事件 * event-name 爲事件名稱,注意不要和現有的事件重名 * params 爲附帶的參數 */ compiler.apply('event-name',params); /** * 監聽名稱爲 event-name 的事件,當 event-name 事件發生時,函數就會被執行。 * 同時函數中的 params 參數爲廣播事件時附帶的參數。 */ compiler.plugin('event-name',function(params) { }); 複製代碼
同理,compilation.apply 和 compilation.plugin 使用方法和上面一致。
在開發插件時,你可能會不知道該如何下手,由於你不知道該監聽哪一個事件才能完成任務。
在開發插件時,還須要注意如下兩點:
compiler.plugin('emit',function(compilation, callback) { // 支持處理邏輯 // 處理完畢後執行 callback 以通知 Webpack // 若是不執行 callback,運行流程將會一直卡在這不往下執行 callback(); }); 複製代碼
插件能夠用來修改輸出文件、增長輸出文件、甚至能夠提高 Webpack 性能、等等,總之插件經過調用 Webpack 提供的 API 能完成不少事情。 因爲 Webpack 提供的 API 很是多,有不少 API 不多用的上,又加上篇幅有限,下面來介紹一些經常使用的 API。
有些插件可能須要讀取 Webpack 的處理結果,例如輸出資源、代碼塊、模塊及其依賴,以便作下一步處理。
在 emit
事件發生時,表明源文件的轉換和組裝已經完成,在這裏能夠讀取到最終將輸出的資源、代碼塊、模塊及其依賴,而且能夠修改輸出資源的內容。 插件代碼以下:
class Plugin { apply(compiler) { compiler.plugin('emit', function (compilation, callback) { // compilation.chunks 存放全部代碼塊,是一個數組 compilation.chunks.forEach(function (chunk) { // chunk 表明一個代碼塊 // 代碼塊由多個模塊組成,經過 chunk.forEachModule 能讀取組成代碼塊的每一個模塊 chunk.forEachModule(function (module) { // module 表明一個模塊 // module.fileDependencies 存放當前模塊的全部依賴的文件路徑,是一個數組 module.fileDependencies.forEach(function (filepath) { }); }); // Webpack 會根據 Chunk 去生成輸出的文件資源,每一個 Chunk 都對應一個及其以上的輸出文件 // 例如在 Chunk 中包含了 CSS 模塊而且使用了 ExtractTextPlugin 時, // 該 Chunk 就會生成 .js 和 .css 兩個文件 chunk.files.forEach(function (filename) { // compilation.assets 存放當前全部即將輸出的資源 // 調用一個輸出資源的 source() 方法能獲取到輸出資源的內容 let source = compilation.assets[filename].source(); }); }); // 這是一個異步事件,要記得調用 callback 通知 Webpack 本次事件監聽處理結束。 // 若是忘記了調用 callback,Webpack 將一直卡在這裏而不會日後執行。 callback(); }) } } 複製代碼
在4-5使用自動刷新 中介紹過 Webpack 會從配置的入口模塊出發,依次找出全部的依賴模塊,當入口模塊或者其依賴的模塊發生變化時, 就會觸發一次新的 Compilation。
在開發插件時常常須要知道是哪一個文件發生變化致使了新的 Compilation,爲此可使用以下代碼:
// 當依賴的文件發生變化時會觸發 watch-run 事件 compiler.plugin('watch-run', (watching, callback) => { // 獲取發生變化的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式爲鍵值對,鍵爲發生變化的文件路徑。 if (changedFiles[filePath] !== undefined) { // filePath 對應的文件發生了變化 } callback(); }); 複製代碼
默認狀況下 Webpack 只會監視入口和其依賴的模塊是否發生變化,在有些狀況下項目可能須要引入新的文件,例如引入一個 HTML 文件。 因爲 JavaScript 文件不會去導入 HTML 文件,Webpack 就不會監聽 HTML 文件的變化,編輯 HTML 文件時就不會從新觸發新的 Compilation。 爲了監聽 HTML 文件的變化,咱們須要把 HTML 文件加入到依賴列表中,爲此可使用以下代碼:
compiler.plugin('after-compile', (compilation, callback) => { // 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監聽 HTML 模塊文件,在 HTML 模版文件發生變化時從新啓動一次編譯 compilation.fileDependencies.push(filePath); callback(); }); 複製代碼
有些場景下插件須要修改、增長、刪除輸出的資源,要作到這點須要監聽 emit
事件,由於發生 emit
事件時全部模塊的轉換和代碼塊對應的文件已經生成好, 須要輸出的資源即將輸出,所以 emit
事件是修改 Webpack 輸出資源的最後時機。
全部須要輸出的資源會存放在 compilation.assets
中,compilation.assets
是一個鍵值對,鍵爲須要輸出的文件名稱,值爲文件對應的內容。
設置 compilation.assets
的代碼以下:
compiler.plugin('emit', (compilation, callback) => { // 設置名稱爲 fileName 的輸出資源 compilation.assets[fileName] = { // 返回文件內容 source: () => { // fileContent 既能夠是表明文本文件的字符串,也能夠是表明二進制文件的 Buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, 'utf8'); } }; callback(); }); 複製代碼
讀取 compilation.assets
的代碼以下:
compiler.plugin('emit', (compilation, callback) => { // 讀取名稱爲 fileName 的輸出資源 const asset = compilation.assets[fileName]; // 獲取輸出資源的內容 asset.source(); // 獲取輸出資源的文件大小 asset.size(); callback(); }); 複製代碼
在開發一個插件時可能須要根據當前配置是否使用了其它某個插件而作下一步決定,所以須要讀取 Webpack 當前的插件配置狀況。 以判斷當前是否使用了 ExtractTextPlugin 爲例,可使用以下代碼:
// 判斷當前配置使用使用了 ExtractTextPlugin, // compiler 參數即爲 Webpack 在 apply(compiler) 中傳入的參數 function hasExtractTextPlugin(compiler) { // 當前配置全部使用的插件列表 const plugins = compiler.options.plugins; // 去 plugins 中尋找有沒有 ExtractTextPlugin 的實例 return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null; } 複製代碼
下面咱們舉一個實際的例子,帶你一步步去實現一個插件。
該插件的名稱取名叫 EndWebpackPlugin,做用是在 Webpack 即將退出時再附加一些額外的操做,例如在 Webpack 成功編譯和輸出了文件後執行發佈操做把輸出的文件上傳到服務器。 同時該插件還能區分 Webpack 構建是否執行成功。使用該插件時方法以下:
module.exports = { plugins:[ // 在初始化 EndWebpackPlugin 時傳入了兩個參數,分別是在成功時的回調函數和失敗時的回調函數; new EndWebpackPlugin(() => { // Webpack 構建成功,而且文件輸出了後會執行到這裏,在這裏能夠作發佈文件操做 }, (err) => { // Webpack 構建失敗,err 是致使錯誤的緣由 console.error(err); }) ] } 複製代碼
要實現該插件,須要藉助兩個事件:
實現該插件很是簡單,完整代碼以下:
class EndWebpackPlugin { constructor(doneCallback, failCallback) { // 存下在構造函數中傳入的回調函數 this.doneCallback = doneCallback; this.failCallback = failCallback; } apply(compiler) { compiler.plugin('done', (stats) => { // 在 done 事件中回調 doneCallback this.doneCallback(stats); }); compiler.plugin('failed', (err) => { // 在 failed 事件中回調 failCallback this.failCallback(err); }); } } // 導出插件 module.exports = EndWebpackPlugin; 複製代碼
從開發這個插件能夠看出,找到合適的事件點去完成功能在開發插件時顯得尤其重要。 在 5-1工做原理歸納 中詳細介紹過 Webpack 在運行過程當中廣播出經常使用事件,你能夠從中找到你須要的事件。
本實例提供項目完整代碼