首頁 > 俗語

PC 前端大型單頁式的 JS 模組化構建探索

作者:由 文江部落格 發表于 俗語日期:2023-02-01

精益求精怎麼讀

前提

為了不被噴得太慘,給標題加了這麼多的限制定語也是相當不容易的了。此文討論的是我所處的環境下對JavaScript構建的一些簡單探索,因此有相當多的前提限制。

首先,何為大型。從我們的系統來看,20 多個業務模組,近 100 個頁面組成的單頁系統,對應的業務原始碼程式碼量如下:

PC 前端大型單頁式的 JS 模組化構建探索

對應的依賴庫,除

underscore

moment

外均為公司內部庫,程式碼量為:

PC 前端大型單頁式的 JS 模組化構建探索

其次,所謂的“模組化”指我們使用AMD進行構建,使用符合社群AMD標準的Loader進行模組的載入。

而“PC 端單頁式商業內容管理系統”則代表著系統的不少特性:

使用是相對強制的,對使用者來說這是一項工作,而不是愛用不用的使用者產品。

商業公司通常擁有較好的網路環境,面向PC設計更使得頻寬不是一個需要著重考慮的因素。

單頁系統使得所有功能被包含在一個HTML頁面內,不存在頁間的跳轉,因此資源不以頁面為單位進行切分。

為何要構建

第一個問題是,AMD 有自然的按需載入的屬性,按需載入也是一直被提倡的一種模式。那麼,如果不進行任何的構建,讓模組自然地按需載入,是否可行?

如果看了這個圖,你還相信按需載入的話,可以停止此文的閱讀了:

PC 前端大型單頁式的 JS 模組化構建探索

簡單來說,按需載入與構建並不衝突,我們不能將所有資源最細粒度地使用按需載入進行管理,必要的構建來減少資源請求是必要的。

隨之而來的,我們會考慮標準的程式碼合併方案。相當多的站點會將所有的JavaScript合併為一個檔案,這也是最簡單粗暴有效的方案。

但是對於大型的單頁系統而言,所有JavaScript合併後生成的檔案會非常之巨大,其體積在瀏覽器單執行緒的下載模式下已經成為系統的效能瓶頸。因此我們需要一些更好的策略,讓系統的啟動效能得以最佳化。

最後,常用於業界的還有一種方案,即自動化的執行時合併。透過在伺服器端配置一個處理程式,可以執行時檢測需要檔案的依賴,進行依賴打包並響應至客戶端。

這種方案有其成本小、透明化等多方面的優勢,但在精益求精的場景下仍舊略有不足。其最大的缺點是當有2個以上模組依賴同一個模組時,被依賴模組可能會被重複打包到多份

。js

檔案中,造成不必要的網路傳輸。

當然有很多的方法解決這一問題,諸如在Session中記錄使用者已經擁有的模組,或由客戶端記錄並提供已有模組列表,來保證打包過程不會加入無用的模組。但這些方法會提升一定的開發成本,同時前後端合作才可以完成的方案往往在推進上會遇到一些小阻礙。

基於這些原因,從前端靜態化的構建入手,在構建階段實現較為最佳化的打包方案,是現階段我們採取的策略。

準則

從系統執行時來分析,對於JavaScript的構建,可以提出以下的原則。

控制請求段的數量

“請求段”是一個很模糊的概念,簡單來說,在一個頻寬足夠的環境下,我們並不看重執行時產生了多少個請求,而是看重這些請求在瀑布圖中被分為幾段。由於瀏覽器並行載入的特性,系統真正的可用時間是由段的數量和每一個段的時間來決定的。

對於常見的瀏覽器,其並行載入的請求個數為4-6個,也即一個段可以加入4-6個的請求。從分段越少越好的角度來考慮,我們規劃的系統啟動分為3個段:

載入必要的前置條件,其中最為主要的是AMD Loader。

。css

檔案可以在這個階段載入,以避免影響後面更重量級的

。js

的載入效率。

主要的JavaScript模組的載入,在此段透過對並行數的控制,期望在一個段內載入完所有必要的模組。同時從

段的用時 = 段內載入總大小 / 併發數

這一公式考慮,應儘可能將模組打包為瀏覽器可接受的最大併發數個檔案。

一些動態的資訊,如使用者登入資訊、系統常量表等,這些資訊是易變或動態的,因此從快取的角度考慮不適合進行打包。

從我們的系統來看,僅看JavaScript的資源,很明顯地分為3段進行載入(紅線分隔):

PC 前端大型單頁式的 JS 模組化構建探索

檔案大小

從經驗值來看,單個資源的大小盡量控制在未Gzip前500KB以內,過大的檔案會成為瓶頸,除非你可以很好地規劃一個HTTP請求段,使得一個大檔案載入的用時內瀏覽器會利用另一個TCP連結載入多個小檔案。

Gzip對純文字檔案的壓縮率一般在16%左右,檔案的形式和內容不會對此比率造成特別大的影響。如果使用Linux或OSX系統,可以簡單地使用以下命令來看一個檔案Gzip後的近似大小:

gzip -c {file} | wc -c

同時,由於短板效應的存在,一個段的載入時間會由這個段內最大的資源決定,因此儘量使得各資源的大小相近。

請求控制

並不是所有的檔案都需要合併在一起,也不是所有資源都可以按需載入,對於請求數量的控制並不僅僅體現在系統啟動時,也貫穿整個系統的使用流程,來提供使用者一致的效能體驗。

我對一個大型CMS系統的請求數量的總結可以概括為3點:

在導航透過一次操作可到達的頁面內,不應該產生額外的請求,即系統啟動過程中這些模組都應當就位。

在一級頁面中進行下探才可以到達的頁面,儘量控制一個頁面僅產生一個請求。考慮到單個頁面的資源不會很大,因此再行拆分並行載入反而可能因為TCP連結、網路延遲等因素有負面的效果。

對於特別大的頁面,或者頁面中一定條件下才會訪問的區域,相關資源使用按需載入的策略配置。

基於以上的這些原則,我在系統中有進一步的實踐。

實踐

檔案合併

將JavaScript檔案分為多個“啟動指令碼”,一個啟動指令碼中會包含一系列的模組,現有系統中我將啟動指令碼分割為4個,分別為:

特別大的庫單獨擁有自己的一個指令碼,比如

ECharts

之類的圖表庫。

UI控制元件庫,包含基礎UI控制元件和業務UI控制元件,合併為一個指令碼。

MVC框架、頁面基類、工具類、系統通用功能層等業務無關的邏輯合併為一個指令碼。

一級頁面的業務模組合併為一個指令碼。

需要特別注意的是,各啟動指令碼間應該保持完美的正交,即不應該有任何一個模組被重複合併到多個指令碼中。

除了啟動指令碼外,前面也有提到單一頁面應該儘可能只加載一個資源,因此頁面相關會被打包在一起,比較典型的是將Controller、Model和View合併到Controller對應的檔案中。

由於二級頁面並不能確定哪一個會被先訪問,因此各頁面打包檔案中是會存在一定的模組重複的,經典如

util

模組就會同時被列表、表單、只讀等頁使用。這會導致載入多個頁面時部分資源被重複載入,但是此類資源通常體積很小,產生的副作用在可控範圍內。

檔案載入

在檔案載入這一方向上,需要有一個特別的處理。由於AMD的依賴管理和執行時依賴分析功能,透過Loader的

require

函式載入一個模組的化,Loader會自動分析依賴並透過零碎的HTTP請求去請求相關的資源,而無視這些資源是否可能被下一個指令碼打包在一起。

用一個例項來說明,我們的依賴關係為

a -> b

以及

c -> b

,即

b

模組是一個通用模組,被兩邊所依賴。當我們將

a

c

分開打包為2個檔案時,

b

會出現在其中一箇中(為了實現完美正交)。假設

b

被打包在

a。js

中,那麼當

c。js

被Loader載入時,如果

a。js

還未就位,就會產生一個單獨的HTTP請求

b。js

。由於並行下載時,誰先完成是不可預知的,就有很大的可能性產生無意義的零碎請求。

解決這一問題的方法是,使用