【轉載】前端工程精粹(二):靜態資源管理與模板框架

本系列文章從一個全新的視角來思考web性能優化與前端工程之間的關係,經過解讀百度前端集成解決方案小組(F.I.S)在打造高性能前端架構並統一百度40多條前端產品線的過程當中所經歷的技術嘗試,揭示前端性能優化在前端架構及開發工具設計層面的實現思路。php

在上一部分,咱們介紹了靜態資源版本更新與緩存。今天的部分將會介紹靜態資源管理與模板框架的用法。css

靜態資源管理與模板框架

讓咱們再來看看前面的優化原則表還剩些什麼:html

優化方向前端

優化手段python

請求數量jquery

合併腳本和樣式表,拆分初始化負載git

請求帶寬github

移除重複腳本web

緩存利用算法

使Ajax可緩存

頁面結構

將樣式表放在頂部,將腳本放在底部,儘早刷新文檔的輸出                                                                   

很不幸,剩下的優化原則都不是使用工具就能很好實現的。或許有人會辯駁:「我用某某工具能夠實現腳本和樣式表合併」。嗯,必須認可,使用工具進行資源合併並替換引用或許是一個不錯的辦法,但在大型web應用,這種方式有一些很是嚴重的缺陷,來看一個很熟悉的例子:

某個web產品頁面有A、B、C三個資源

工程師根據「減小HTTP請求」的優化原則合併了資源

 

產品經理要求C模塊按需出現,此時C資源已出現多餘的可能

 

C模塊再也不須要了,註釋掉吧!但C資源一般不敢輕易剔除

 

不知不覺中,性能優化變成了性能惡化……

 

事實上,使用工具在線下進行靜態資源合併是沒法解決資源按需加載的問題的。若是解決不了按需加載,則勢必會致使資源的冗餘;此外,線下經過工具實現的資源合併一般會使得資源加載和使用的分離,好比在頁面頭部或配置文件中寫資源引用及合併信息,而用到這些資源的html組件寫在了頁面其餘地方,這種書寫方式在工程上很是容易引發維護不一樣步的問題,致使使用資源的代碼刪除了,引用資源的代碼卻還在的狀況。所以,在工業上要實現資源合併至少要知足以下需求:

  1. 確實能減小HTTP請求,這是基本要求(合併)
  2. 在使用資源的地方引用資源(就近依賴),不使用不加載(按需)
  3. 雖然資源引用不是集中書寫的,但資源引用的代碼最終還能出如今頁面頭部(css)或尾部(js)
  4. 可以避免重複加載資源(去重)

將以上要求綜合考慮,不難發現,單純依靠前端技術或者工具處理是很難達到這些理想要求的。現代大型web應用所展現的頁面絕大多數都是使用服務端動態語言拼接生成的。有的產品使用模板引擎,好比smarty、velocity,有的則乾脆直接使用動態語言,好比php、python。不管使用哪一種方式實現,前端工程師開發的html絕大多數最終都不是以靜態的html在線上運行的。

接下來我會講述一種新的模板架構設計,用以實現前面說到那些性能優化原則,同時知足工程開發和維護的須要,這種架構設計的核心思想就是:

基於依賴關係表的靜態資源管理系統與模板框架設計

考慮一段這樣的頁面代碼:

根據資源合併需求中的第二項,咱們但願資源引用與使用能儘可能靠近,這樣未來維護起來會更容易一些,所以,理想的源碼是:

固然,把這樣的頁面直接送達給瀏覽器用戶是會有嚴重的頁面閃爍問題的,因此咱們實際上仍然但願最終頁面輸出的結果仍是如最開始的截圖同樣,將css放在頭部輸出。這就意味着,頁面結構須要有一些調整,而且有能力收集資源加載需求,那麼咱們考慮一下這樣的源碼:

在頁面的頭部插入一個html註釋「<!--[CSS LINKS PLACEHOLDER]-->」做爲佔位,而將原來字面書寫的資源引用改爲模板接口(require)調用,該接口負責收集頁面所需資源。require接口實現很是簡單,就是準備一個數組,收集資源引用,而且能夠去重。最後在頁面輸出的前一刻,咱們將require在運行時收集到的「A.css」、「B.css」、「C.css」三個資源拼接成html標籤,替換掉註釋佔位「<!--[CSS LINKS PLACEHOLDER]-->」,從而獲得咱們須要的頁面結構。

通過fis團隊的總結,咱們發現模板層面只要實現三個開發接口,既能夠比較完美的實現目前遺留的大部分性能優化原則,這三個接口分別是:

  1. require(String id):收集資源加載需求的接口,參數是資源id。
  2. widget(String template_id):加載拆分紅小組件模板的接口。你能夠叫它爲load、component或者pagelet之類的。總之,咱們須要一個接口把一個大的頁面模板拆分紅一個個的小部分來維護,最後在原來的大頁面以組件爲單位來加載這些小部件。
  3. script(String code):收集寫在模板中的js腳本,使之出現的頁面底部,從而實現性能優化原則中的「將js放在頁面底部」原則。

實現了這些接口以後,一個重構後的模板頁面的源代碼可能看起來就是這樣的了:

而最終在模板解析的過程當中,資源收集與去重、頁面script收集、佔位符替換操做,最終從服務端發送出來的html代碼爲:

不難看出,咱們目前已經實現了「按需加載」,「將腳本放在底部」,「將樣式表放在頭部」三項優化原則。

前面講到靜態資源在上線後須要添加hash戳做爲版本標識,那麼這種使用模板語言來收集的靜態資源該如何實現這項功能呢?答案是:靜態資源依賴關係表

假設前面講到的模板源代碼所對應的目錄結構爲下圖所示:

那麼咱們可使用工具掃描整個project目錄,而後建立一張資源表,同時記錄每一個資源的部署路徑,能夠獲得這樣的一張表:

基於這張表,咱們就很容易實現 {require name=」id」} 這個模板接口了。只須查表便可。好比執行{require name=」jquery.js」},查表獲得它的url是「/jquery_9151577.js」,聲明一個數組收集起來就行了。這樣,整個頁面執行完畢以後,收集資源加載需求,並替換頁面的佔位符,便可實現資源的hash定位,獲得:

接下來,咱們討論如何在基於表的設計思想上是如何實現靜態資源合併的。或許有些團隊使用過combo服務,也就是咱們在最終拼接生成頁面資源引用的時候,並非生成多個獨立的link標籤,而是將資源地址拼接成一個url路徑,請求一種線上的動態資源合併服務,從而實現減小HTTP請求的需求,好比:

這個「/combo?files=file1,file2,file3,…」的url請求響應就是動態combo服務提供的,它的原理很簡單,就是根據get請求的files參數找到對應的多個文件,合併成一個文件來響應請求,並將其緩存,以加快訪問速度。

這種方法很巧妙,有些服務器甚至直接集成了這類模塊來方便的開啓此項服務,這種作法也是大多數大型web應用的資源合併作法。但它也存在一些缺陷:

  1. 瀏覽器有url長度限制,所以不能無限制的合併資源。
  2. 若是用戶在網站內有公共資源的兩個頁面間跳轉訪問,因爲兩個頁面的combo的url不同致使用戶不能利用瀏覽器緩存來加快對公共資源的訪問速度。

對於上述第二條缺陷,能夠舉個例子來看說明:

  • 假設網站有兩個頁面A和B
  • A頁面使用了a,b,c,d四個資源
  • B頁面使用了a,b,e,f四個資源
  • 若是使用combo服務,咱們會得:
    • A頁面的資源引用爲:/combo?files=a,b,c,d
    • B頁面的資源引用爲:/combo?files=a,b,e,f
  • 兩個頁面引用的資源是不一樣的url,所以瀏覽器會請求兩個合併後的資源文件,跨頁面訪問沒能很好的利用a、b這兩個資源的緩存。

很明顯,若是combo服務能聰明的知道A頁面使用的資源引用爲「/combo?files=a,b」和「/combo?files=c,d」,而B頁面使用的資源引用爲「/combo?files=a,b」,「/combo?files=e,f」就行了。這樣當用戶在訪問A頁面以後再訪問B頁面時,只須要下載B頁面的第二個combo文件便可,第一個文件已經在訪問A頁面時緩存好了的。

基於這樣的思考,fis在資源表上新增了一個字段,取名爲「pkg」,就是資源合併生成的新資源,表的結構會變成:

相比以前的表,能夠看到新表中多了一個pkg字段,而且記錄了打包後的文件所包含的獨立資源。這樣,咱們從新設計一下{require name=」id」}這個模板接口:在查表的時候,若是一個靜態資源有pkg字段,那麼就去加載pkg字段所指向的打包文件,不然加載資源自己。好比執行{require name=」bootstrap.css」},查表得知bootstrap.css被打包在了「p0」中,所以取出p0包的url「/pkg/utils_b967346.css」,而且記錄頁面已加載了「bootstrap.css」和「A/A.css」兩個資源。這樣一來,以前的模板代碼執行以後獲得的html就變成了:

css資源請求數由原來的4個減小爲2個。

這樣的打包結果是怎麼來的呢?答案是配置獲得的。

咱們來看一下帶有打包結果的資源表的fis配置:

咱們將「bootstrap.css」、「A/A.css」打包在一塊兒,其餘css另外打包,從而生成兩個打包文件,當頁面須要打包文件中的資源時,模塊框架就會收集並計算出最優的資源加載結果,從而解決靜態資源合併的問題。

這樣作的緣由是爲了彌補combo在前面講到的兩點技術上的不足而設計的。但也不難發現這種打包策略是須要配置的,這就意味着維護成本的增長。但好在它有兩個優點能夠必定程度上彌補這個問題:

  1. 打包的資源只是原來獨立資源的備份。打包與否不會致使資源的丟失,最可能是沒有合併的很好而已。
  2. 配置能夠由工程師根據經驗人工維護,也能夠由統計日誌生成,這爲性能優化自適應網站設計提供了很是好的基礎。

關於第二點,fis有這樣輔助系統來支持自適應打包算法:

至此,咱們經過基於表的靜態資源管理系統和三個模板接口實現了幾個重要的性能優化原則,如今咱們再來回顧一下前面的性能優化原則分類表,剔除掉已經作到了的,看看還剩下哪些沒作到的:

優化方向

優化手段

請求數量

拆分初始化負載

緩存利用

使Ajax可緩存

頁面結構

儘早刷新文檔的輸出                                                                                                                                    

「拆分初始化負載」的目標是將頁面一開始加載時不須要執行的資源從全部資源中分離出來,等到須要的時候再加載。工程師一般沒有耐心去區分資源的分類狀況,但咱們能夠利用組件化框架接口來幫助工程師管理資源的使用。仍是從例子開始思考:

模板源代碼

在fis給百度內部團隊開發的架構中,若是這樣書寫代碼,頁面最終的執行結果會變成:

模板運行後輸出的html代碼

fis系統會分析頁面中require(id)函數的調用,並將依賴關係記錄到資源表對應資源的deps字段中,從而在頁面渲染查表時能夠加載依賴的資源。但此時dialog.js是以script標籤的形式同步加載的,這樣會在頁面初始化時出現資源的浪費。所以,fis團隊提供了require.async的接口,用於異步加載一些資源,源碼修改成:

這樣書寫以後,fis系統會在表裏以async字段來標準資源依賴關係是異步的。fis提供的靜態資源管理系統會將頁面輸出的結果修改成:

dialog.js不會在頁面以script src的形式輸出,而是變成了資源註冊,這樣,當頁面點擊按鈕觸發require.async執行的時候,async函數纔會查表找到資源的url並加載它,加載完畢後觸發回調函數。

到目前爲止,咱們又以架構的形式實現了一項優化原則(拆分初始化負載),回顧咱們的優化分類表,如今僅有兩項沒能作到了:

優化方向

優化手段

緩存利用

使Ajax可緩存

頁面結構

儘早刷新文檔的輸出                                                                                                                              

剩下的兩項優化原則要作到並不容易,真正可緩存的Ajax在現實開發中比較少見,而儘早刷新文檔的輸出的狀況facebook在2010年的velocity上提到過,就是BigPipe技術。當時facebook團隊還講到了Quickling和PageCache兩項技術,其中的PageCache算是比較完全的實現Ajax可緩存的優化原則了。fis團隊也曾與某產品線合做基於靜態資源表、模板組件化等技術實現了頁面的PipeLine輸出、以及Quickling和PageCache功能,但最終效果沒有達到理想的性能優化預期,所以這兩個方向尚在探索中,相信在不久的未來會有新的突破。

總結

其實在前端開發工程管理領域還有不少細節值得探索和挖掘,提高前端團隊生產力水平並非一句空話,它須要咱們能對前端開發及代碼運行有更深入的認識,對性能優化原則有更細緻的分析與研究。fis團隊一直致力於從架構而非經驗的角度實現性能優化原則,解決前端工程師開發、調試、部署中遇到的工程問題,提供組件化框架,提升代碼複用率,提供開發工具集,提高工程師的開發效率。在前端工業化開發的全部環節均有可節省的人力成本,這些成本很是可觀,相信如今不少大型互聯網公司也都有了這樣的共識。

本文只是將這個領域中很小的一部分知識的展開討論,拋磚引玉,但願能爲業界相關領域的工做者提供一些不同的思路。歡迎關注fis項目,對本文有任何意見或建議均可以在fis開源項目中進行反饋和討論。

 

做者簡介:張雲龍,百度公司Web前端研發部前端集成解決方案小組的技術負責人,目前負責F.I.S項目,讀者能夠關注他的微博:http://weibo.com/fouber/

相關文章
相關標籤/搜索