Webpack原理-編寫Plugin
Webpack 通過 Plugin 機制讓其更加靈活,以適應各種應用場景。
在 Webpack 運行的生命周期中會廣播出許多事件,Plugin 可以監聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結果。一個最基礎的 Plugin 的代碼是這樣的:
class BasicPlugin{ // 在構造函數中獲取用戶給該插件傳入的配置 constructor(options){ } // Webpack 會調用 BasicPlugin 實例的 apply 方法給插件實例傳入 compiler 對象 apply(compiler){ compiler.plugin(compilation,function(compilation) { }) }}// 導出 Pluginmodule.exports = BasicPlugin;
在使用這個 Plugin 時,相關配置代碼如下:
const BasicPlugin = require(./BasicPlugin.js);module.export = { plugins:[ new BasicPlugin(options), ]}
Webpack 啟動後,在讀取配置的過程中會先執行 new BasicPlugin(options)
初始化一個 BasicPlugin 獲得其實例。
在初始化 compiler 對象後,再調用 basicPlugin.apply(compiler)
給插件實例傳入 compiler 對象。
compiler.plugin(事件名稱, 回調函數)
監聽到 Webpack 廣播出來的事件。並且可以通過 compiler 對象去操作 Webpack。通過以上最簡單的 Plugin 相信你大概明白了 Plugin 的工作原理,但實際開發中還有很多細節需要注意,下面來詳細介紹。
Compiler 和 Compilation
在開發 Plugin 時最常用的兩個對象就是 Compiler 和 Compilation,它們是 Plugin 和 Webpack 之間的橋樑。
Compiler 和 Compilation 的含義如下:- Compiler 對象包含了 Webpack 環境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啟動時候被實例化,它是全局唯一的,可以簡單地把它理解為 Webpack 實例;
- Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被創建。Compilation 對象也提供了很多事件回調供插件做擴展。通過 Compilation 也能讀取到 Compiler 對象。
Compiler 和 Compilation 的區別在於:Compiler 代表了整個 Webpack 從啟動到關閉的生命周期,而 Compilation 只是代表了一次新的編譯。
事件流
Webpack 就像一條生產線,要經過一系列處理流程後才能將源文件轉換成輸出結果。
這條生產線上的每個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。插件就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源做處理。Webpack 通過 Tapable 來組織這條複雜的生產線。
Webpack 在運行過程中會廣播事件,插件只需要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運作。Webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。Webpack 的事件流機制應用了觀察者模式,和 Node.js 中的 EventEmitter 非常相似。
Compiler 和 Compilation 都繼承自 Tapable,可以直接在 Compiler 和 Compilation 對象上廣播和監聽事件,方法如下:/*** 廣播出事件* event-name 為事件名稱,注意不要和現有的事件重名* params 為附帶的參數*/compiler.apply(event-name,params);/*** 監聽名稱為 event-name 的事件,當 event-name 事件發生時,函數就會被執行。* 同時函數中的 params 參數為廣播事件時附帶的參數。*/compiler.plugin(event-name,function(params) { });
同理,compilation.apply 和 compilation.plugin 使用方法和上面一致。
在開發插件時,你可能會不知道該如何下手,因為你不知道該監聽哪個事件才能完成任務。
在開發插件時,還需要注意以下兩點:
- 只要能拿到 Compiler 或 Compilation 對象,就能廣播出新的事件,所以在新開發的插件中也能廣播出事件,給其它插件監聽使用。
- 傳給每個插件的 Compiler 和 Compilation 對象都是同一個引用。也就是說在一個插件中修改了 Compiler 或 Compilation 對象上的屬性,會影響到後面的插件。
- 有些事件是非同步的,這些非同步的事件會附帶兩個參數,第二個參數為回調函數,在插件處理完任務時需要調用回調函數通知 Webpack,才會進入下一處理流程。例如:
compiler.plugin(emit,function(compilation, callback) {
// 處理完畢後執行 callback 以通知 Webpack
// 如果不執行 callback,運行流程將會一直卡在這不往下執行
callback();
});
常用 API
插件可以用來修改輸出文件、增加輸出文件、甚至可以提升 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 使用了哪些插件
在開發一個插件時可能需要根據當前配置是否使用了其它某個插件而做下一步決定,因此需要讀取 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); }) ]}
要實現該插件,需要藉助兩個事件:
- done:在成功構建並且輸出了文件後,Webpack 即將退出時發生;
- failed:在構建出現異常導致構建失敗,Webpack 即將退出時發生;
實現該插件非常簡單,完整代碼如下:
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 在運行過程中廣播出常用事件,你可以從中找到你需要的事件。本實例提供項目完整代碼
《深入淺出Webpack》全書在線閱讀鏈接
閱讀原文
推薦閱讀:
※加速Webpack-縮小文件搜索範圍
※webpack 4 終於知道「約定優於配置」了
※web front end Automation Tools
※讀懂webpack生成的26行代碼
※Egg + Vue 服務端渲染工程化實現
TAG:webpack |