淺析 Webpack 插件化設計

題圖:twitter.com/richavyas02

前言

在專欄之前的一篇 《從 Bundle 文件看 Webpack 模塊機制》中,我們了解到經過 Webpack 構建之後的代碼是如何工作的,學習了其模塊化的實現原理,今天將帶大家走進 Webpack 的本體設計中去,從宏觀的角度觀察其內部的運行和實現。

Webpack 代碼較為複雜,其在內部高度使用事件通信和插件化的機制來實現代碼解耦及工作流程的控制。本文將以 Webpack 的 2.3.3 版本為例進行相關演示。

事件系統

說起事件,自然少不了發布/訂閱模式,Webpack 的基礎組件之一 Tapable 是為其量身定做的 「EventEmitter」,但它不只是單純的事件中樞,還相應補充了對事件流程的控制能力,增加如 waterfall/parallel 系列方法,實現了非同步/並行等事件流的控制能力。

以下截選了實例的部分 API,具體可參閱源碼 [tapable/Tapable.js at master · webpack/tapable · GitHub],僅三百餘行。

總體來說,可分為三類方法:

  1. apply 提供給插件的註冊使用。
  2. plugins 註冊事件監聽,接受事件名稱和對應的回調函數。
  3. applyPlugins[xx] 系列方法用於 emit 事件。類似 applyPlugins0, applyPlugins1... 這樣後面的數字用來限制對應事件函數形參個數,類似參數限制的聲明保證了介面聲明的一致性。

事件註冊相關源碼如下,註冊的事件維護在 _plugins 對象中。

Tapable.prototype.plugin = function plugin(name, fn) {n if(Array.isArray(name)) {n name.forEach(function(name) {n this.plugin(name, fn);n }, this);n return;n }n if(!this._plugins[name]) this._plugins[name] = [fn];n else this._plugins[name].push(fn);n};nnTapable.prototype.apply = function apply() {n for(var i = 0; i < arguments.length; i++) {n arguments[i].apply(this);n }n};n

Webpack 的核心對象 Compiler/Compilation 都是 Tapable 的子類,各自分管自己的_plugins。由 Compiler 提供的 Event Hook 往往也對應著 Bundle 過程的各生命周期,從中獲可以取到對應階段的 Compilation 對象引用,Compilation 是主要的執行者,在相應周期中負責各項子任務,並觸發更細粒度的事件,同時保持著對處理結果的引用。對於開發者編碼主要集中在 Compilation hook 的處理上,用於捕獲事件結果進行二次改造。

插件化設計

Webpack 的插件與事件是緊密相連的,插件的設計讓代碼高度職能化,事件如同絲線連接,完成插件與主體(主要為 Compiler 和 Ccompilation)間的流程和數據的協作。

一、插件定義

插件的介面也是非常簡單,僅需要實現一個 apply 方法,這與 Tapable 的 apply 方法相對應,具體文檔可參考官方的 How to write a plugin。

我們每天都在使用各種 Webpack Plugins 完成項目的構建,有時也需要自己為項目量身定製。Webpack 內部插件與這些日常使用插件完全相同,不僅遵循一致的 API 設計,也共享相同的事件發布者。也就是說,我們可以通過外部插件觸及到 Bundle 過程的每個階段,高度的拓展性也是 Webpack 社區繁榮的基礎之一。

二、插件註冊

插件註冊的實質是插件內部事件的註冊。日常使用中,我們將插件實例化以後聲明在配置的 plugins 數組中,Webpack 接收此數組註冊每個插件:

if(options.plugins && Array.isArray(options.plugins)) {n compiler.apply.apply(compiler, options.plugins);n}n

Compiler.apply 順序調用各插件的 apply 方法並傳入 Compilation 運行時對象作為唯一的參數,方法內部調用 Compiler/Compilation 的 plugin 方法完成事件監聽的註冊。

執行實例

下面我們選取 Webpack 模塊熱更新這一過程為例,了解一下事件與插件系統在開發實踐中的應用與表現。

要開啟熱更新,需要在配置對象中聲明 webpack.HotModuleReplacementPlugin 的實例。可以看出,熱更新功能同樣作為一個內置插件拓展在本體中,相應文件位於 /lib/HotModuleReplacementPlugin.js。在此階段所發生的事件調用流大致如下:

  • 判斷是否輸出 assets:applyPluginsBailResult - "should-generate-chunk-assets"
  • 輸出前的 Before 事件:applyPlugins0 - "before-chunk-assets"
  • 輸出結束:applyPlugins2 - "chunk-asset"
  • 附加的 assets 處理階段:applyPlugins1 - "additional-chunk-assets"
  • 若附加階段對需要對 assets 進行操作則再次觸發:applyPlugins - "chunk-asset"

Webpack 的 watch 文件變動後觸發一輪新的 buildModule,生成 chunk 後再次調用 Compilation.createChunkAssets 方法更新 assets 對象,觸發 「chunk-assets」 事件,緊跟著會觸發「additional-chunk-assets」事件,目前源碼中僅有 HotModuleReplacementPlugin 監聽 「additional-chunk-assets」 事件,於是執行權轉移給此插件。在插件內部,通過 Compilation 對象獲取到本輪 build 的 chunk 信息,篩選出更新和移除的 module 交由 Template 對象生成 hot-update.js 的源代碼作為新的 chunk 加入到 assets 中。

值得一提的是,在系統設計考量上,Webpack 並不只局限於滿足自身的實現,還儘可能站在系統拓展的角度把控。如前文所述的 「chunk-assets」 事件其自身並沒有註冊相應的回調函數,但仍然保留這一事件的觸發,傳遞當前階段 chunk 對象的引用和對應的 chunk 文件名,有需求的開發者可以通過這個 hook 對 assets 進行二次開發, 類似細節還有很多。

總結

在事件機制驅動下,通過良好的 API 約定,簡潔的插件系統設計,Webpack 在完成自身繁重的構建任務同時,還提供了良好的拓展性及可測試性。然而事件機制並不是萬金油,如通過事件維繫的代碼缺乏明確的索引關係將增加代碼跟蹤和調試的複雜度。本文作為簡明地分析,希望能拋磚引玉,如有不當之處還望指正。

祝各位擁有一份好心情:)。


推薦閱讀:

Webpack工程化解決方案easywebpack
你的Tree-Shaking並沒什麼卵用
Webpack 之 Loader 的使用
基於 Webpack 3 的 Vue.js 工程項目腳手架
阿里雲前端工程化方案dawn

TAG:webpack | 前端开发 |