Webpack基本架構淺析

Trunks發表於2018-04-19

文章webpack版本為3.6.0

前言

隨著掌握的前端基礎知識越來越多,對技術的要求逐漸不滿足於實現即可,技術到了瓶頸期,自己也曾嘗試寫過一些開源庫,不過很少有滿意的作品,通常沒迭代幾個版本就沒有耐心繼續維護了。通常是面臨的情形是前期設計思路太過簡單導致後期擴充套件的時候需要重構大量的程式碼(GG吧~),就好比一坨屎,再怎麼裝點,都很難把它當成蛋糕吃下去。

我認為,突破這個瓶頸的關鍵就是學會深入理解優秀開源庫背後的思路。有人可能會說,我用xxx已經很久了,能夠熟練使用它解決各種棘手問題,對於它,我已經充分理解了。我想說的是,即便你對於它的使用已經達到了爐火純青的程度,但是站在使用者角度理解再“深”能有多深呢,不過是坐井觀天罷了。

為什麼Webpack

目前為止,Webpack已經擁有39.9k的star,在前端程式碼打包器領域內應該算是無敵的存在了吧。Webpack強大的生態圈和豐富的解決方案使得我們在日常開發中很難逃脫它的魔爪。如果能學習到它背後的思路,對於技能樹的完善和水平層次的提高應該是非常有好處的。

概要

如果要全面總結webpack的實現,估計寫10篇文章都不一定夠。為了更加清晰地get到webpack的設計思路,會隱去webpack的大部分功能實現。

以實現簡單的js模組打包功能為背景,文章分為3部分:

  1. BundleBuilder基本架構
  2. Webpack基本架構
  3. 學到了些什麼

相信你在閱讀完本文後會對Webpack的架構有個大概的瞭解,這應該會對你繼續深入理解webpack其它功能的實現以及編寫外掛會有所幫助。

BundleBuilder基本架構

簡單到不能再簡單的js模組打包器

示意圖

BundleBuilder.JPG-26.6kB

BundleBuilder物件

  1. BundleBuilder物件接收並處理外部配置
  2. 根據配置選擇不同的ModuleResolver
  3. 使用ModuleResolver接收配置得到最終檔案內容
  4. 生成打包後的檔案

ModuleResolver物件

  1. 接收從BundleBuilder傳進的配置
  2. 解析入口檔案內容
  3. 提取子模組路徑,並遞迴地解析子模組
  4. 將引用的模組路徑替換為模組id最終生成模組檔案

webpack基本架構

這個接下來依次講解webpack中幾個重要物件之間的關係,會以各自的視角描述幾個重要的過程。當然,就單單這幾個物件還不能完全地描述流程上的所有內容。

Tapable外掛功能

webpack 4.0的外掛系統已經完全重做並將Tapable更新到了1.0.0

在正式介紹幾個核心物件之前,你需要了解一下Tapable類。

簡單來說,Tapable為一個物件提供了外掛功能。如果你用過Vue.js或者React.js之類的框架,Tapable就是為某個物件提供了相當於元件的生命週期功能,在外部你可以通過呼叫這些生命週期鉤子監聽該物件。

當然,你還可以在外部手動觸發物件的某個生命週期。

如果你想詳細瞭解Tapable的API可以參考這裡(文中版本為0.2.8)

Webpack主函式視角

最巨集觀的視角

1. 合併外部與預設配置

1-1.PNG-220.1kB

2. 配置並建立compiler

1-1.PNG-125kB

3. 在compiler啟動前觸發compiler上的若干生命週期

其中生命週期包括:environment,after-environment,entry-option,after-plugins,after-resolvers

1-3.PNG-122.2kB

4. 啟動compiler

1-4.PNG-107.2kB

5. 將compiler執行後得到的狀態資訊列印出來

1-5.PNG-201.8kB

Compiler視角

1. 正式執行前依次觸發before-run和run生命週期

2-1.JPG-17.1kB

2. 建立params物件並觸發before-compile生命週期

2-2.JPG-16.2kB

3. 觸發compile生命週期並建立compilation物件

2-3.JPG-17.6kB

4. 觸發this-compilation和compilation生命週期

2-4.JPG-17.9kB

5. 觸發make生命週期並呼叫compilation.finish()

在make階段呼叫了compilation.addEntry(),開始構建模組樹,構建完畢後呼叫compilation.finish(),記錄報錯資訊

2-5.JPG-20.6kB

6. 呼叫compilation.seal()並觸發after-compile生命週期

compilation在seal過程中做了很多工作,在compilation視角部分會講到,現在只需知道seal過後compilation生成了assets物件以供compiler生成檔案

2-6.JPG-17.1kB

7. 拿到assets並在生成每個assets對應的檔案

2-7.JPG-22.2kB

8. 將警告資訊和檔案大小資訊合成為stats狀態資訊

2-8.JPG-20kB

9. 觸發done生命週期並將stats狀態資訊交給webpack主函式

2-9.JPG-17.4kB

Compilation構建模組樹視角

當compiler命令compilation構建模組樹之後compilation都做了些什麼

1. 使用moduleFactory建立空module

3-1.JPG-16.6kB

2. 命令module自行構建自身屬性,比如依賴的子模組資訊(dependency)

呼叫module.build()進行構建模組自身屬性

3-2.JPG-24.4kB

3. 遞迴地重複1和2的操作,生成模組樹

3-3.JPG-18.8kB

4. 將模組樹記錄到chunk中

3-4.JPG-19.9kB

Compilation的seal視角

1. 配置chunk

4-1.JPG-25.4kB

2. 將所處模組樹深度和引用順序等資訊記錄在每個模組上

4-2.JPG-37.1kB

3. 將所有模組按照引用順序排序

4-3.JPG-27.5kB

4. 觸發optimize-module-order生命週期並按照排序後的模組順序為每個模組編號

4-4.JPG-28.8kB

5. 使用template物件渲染出chunk的內容source

4-5.JPG-29.2kB

6. 拿到source後生成asset,新增到assets中

4-6.JPG-34.4kB

學到了什麼

引入外掛系統

1. 存在的問題

可以看到,BundleBuilder的架構中完全沒有為第三方提供介面,後期當然也可以做成根據不同的外部配置項來實現一些有限的定製化需求。

但是,這樣為了保證功能的多樣性,會頻繁修改打包器的內部實現。這種做法會使得整個打包器的穩定性不足,最終非常臃腫,維護困難。

2. webpack的做法

反觀webpack,它使用了一種非常聰明的方式。在保證基本架構的前提下,為主流程上的大部分物件都引入外掛系統,使用者可以獲取到這些物件,並且在一些特定的時候執行使用者提供的程式碼。這樣一來,社群的逐漸壯大保證了功能的多樣性,還把穩定性不足的風險留給使用者去處理,提高了整個打包器的可維護性。

過程粒度細化

1. 存在的問題

可以看到,BundleBuilder最終生成檔案內容只有一個過程,就是呼叫ModuleResolver獲取字串。當這個過程中的某一階段需要獨立進行的時候,難免會要重構程式碼。如果內部實現是比較鬆耦合的,那麼重構的工作會比較輕鬆,但是像現在BundleBuilder這種實現,顯然要做的工作並不少。

2. webpack的做法

從接收配置到生成檔案內容,從比較巨集觀的角度,分為構建,封裝,生成檔案內容,三部分。

  1. 保證了內部修改的靈活性。如果要對過程再細分或者新增過程,實現起來會比較方便。
  2. 豐富了對外擴充套件的介面。很顯然,由於webpack引入了外掛系統,細化過程粒度應該是必然選擇,這樣會有效地增加使用者對整個打包過程的自定義能力。
  3. 提升了程式碼的可維護性。當打包器在執行時出現了bug,粒度越小,越加方便定位問題。

更多類的抽象

1. 存在的問題

在BundleBuilder中,對於每個模組僅僅是通過路徑讀取它的檔案內容,然後分析其子模組的資訊,最後生成處理後的模組內容。這些都是過程。如果後面迭代時需要在打包後輸出一些log,如模組警告,模組路徑等與模組相關的資訊。以程式導向的程式設計方式當然也可以實現,但這樣難免會增加實現難度,降低程式碼可讀性。

2. webpack的做法

稍微搜尋一下,不包括自帶外掛,webpack總共有200多個用Class宣告的類。

  1. 結構化的資料。建立一個類就意味著我們能統一很多有相同抽象含義的物件建立同樣的屬性,比如Module類,它可以記錄很多與模組相關的資訊。
  2. 方便擴充套件不同種類的物件。比如模組類,可以通過繼承的方式衍生出,普通js檔案模組,css檔案模組等等。
  3. 多類意味著有承擔不同職責的物件。明確的職責分工,比如compiler僅僅負責compilation的建立,檔案的生成和資訊狀態的合成。ModuleFactory負責建立Module。一旦出了問題方便定位到責任人,降低了各個工作的耦合度。
  4. 物件間的解耦。比如compilation和Module兩個類,webpack其實也可以直接使用compilation來直接建立Module,但是一旦Module的種類增加,不可避免地需要在compilation中寫一些條件語句,這樣,建立Module這部分的程式碼會讓本來就有很多事情要做的compilation變得更加龐大。所以webpack引入了ModuleFactory,compilation只需呼叫ModuleFactory來建立Module就好,建立部分的邏輯則被分佈在了ModuleFactory中,將compilation與Module解耦,兩者中一方發生變化,只需在ModuleFactory中增加邏輯即可。

感受

由於webpack過於龐大,看原始碼的過程感覺是在修行。寫這篇文章之初準備深入到一些技術細節,後來感覺意義不大。也嘗試過列舉在簡單js模組打包流程上涉及到的預設外掛,寫出來像API手冊,如果完全寫完,體量可能都接近半本書了。最後,決定拿小學3年級畫畫水平,將最基本的架構關係畫出來。

最大的感受就是:當你真的準備設計一個庫的時候,應該在實現之前充分列舉可能的應用場景,將充分抽象出穩定的基本架構,然後將難辦的部分,複雜度很高的部分,或者說定製化需求比較多的部分,採用開放外掛的方式扔給使用者去解決。

相關文章