靜態資源管理與模塊化框架
解決了靜態資源緩存問題之后,讓我們再來看看前面的優化原則表還剩些什么:
優化方向 優化手段
請求數量 合并腳本和樣式表,拆分初始化負載
請求帶寬 移除重復腳本
緩存利用 使Ajax可緩存
頁面結構 將樣式表放在頂部,將腳本放在底部,盡早刷新文檔的輸出
很不幸,剩下的優化原則都不是使用工具就能很好實現的。或許有人會辯駁:“我用某某工具可以實現腳本和樣式表合并”。嗯,必須承認,使用工具進行資源合并并替換引用或許是一個不錯的辦法,但在大型web應用,這種方式有一些非常嚴重的缺陷,來看一個很熟悉的例子 :
某個web產品頁面有A、B、C三個資源
工程師根據“減少HTTP請求”的優化原則合并了資源
產品經理要求C模塊按需出現,此時C資源已出現多余的可能
C模塊不再需要了,注釋掉吧!代碼1秒鐘搞定,但C資源通常不敢輕易剔除
不知不覺中,性能優化變成了性能惡化……
這個例子來自 Facebook靜態網頁資源的管理和優化@Velocity China 2010
事實上,使用工具在線下進行靜態資源合并是無法解決資源按需加載的問題的。如果解決不了按需加載,則必會導致資源的冗余;此外,線下通過工具實現的資源合并通常會使得資源加載和使用的分離,比如在頁面頭部或配置文件中寫資源引用及合并信息,而用到這些資源的html組件寫在了頁面其他地方,這種書寫方式在工程上非常容易引起維護不同步的問題,導致使用資源的代碼刪除了,引用資源的代碼卻還在的情況。因此,在工業上要實現資源合并至少要滿足如下需求:
確實能減少HTTP請求,這是基本要求(合并)
在使用資源的地方引用資源(就近依賴),不使用不加載(按需)
雖然資源引用不是集中書寫的,但資源引用的代碼最終還能出現在頁面頭部(css)或尾部(js)
能夠避免重復加載資源(去重)
將以上要求綜合考慮,不難發現,單純依靠前端技術或者工具處理是很難達到這些理想要求的。
接下來我會講述一種新的模板架構設計,用以實現前面說到那些性能優化原則,同時滿足工程開發和維護的需要,這種架構設計的核心思想就是:
基于依賴關系表的靜態資源管理系統與模塊化框架設計
考慮一段這樣的頁面代碼:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="a.css"/>
<link rel="stylesheet" type="text/css" href="b.css"/>
<link rel="stylesheet" type="text/css" href="c.css"/></head><body>
<div> content of module a </div>
<div> content of module b </div>
<div> content of module c </div></body></html>
根據資源合并需求中的第二項,我們希望資源引用與使用能盡量靠近,這樣將來維護起來會更容易一些,因此,理想的源碼是:
<html><head>
<title>page</title></head><body>
<link rel="stylesheet" type="text/css" href="a.css"/>
<div> content of module a </div>
<link rel="stylesheet" type="text/css" href="b.css"/>
<div> content of module b </div>
<link rel="stylesheet" type="text/css" href="c.css"/>
<div> content of module c </div></body></html>
當然,把這樣的頁面直接送達給瀏覽器用戶是會有嚴重的頁面閃爍問題的,所以我們實際上仍然希望最終頁面輸出的結果還是如最開始的截圖一樣,將css放在頭部輸出。這就意味著,頁面結構需要有一些調整,并且有能力收集資源加載需求,那么我們考慮一下這樣的源碼(以php為例):
<html><head>
<title>page</title>
<!--[ CSS LINKS PLACEHOLDER ]--></head><body>
<?php require_static('a.css'); ?>
<div> content of module a </div>
<?php require_static('b.css'); ?>
<div> content of module b </div>
<?php require_static('c.css'); ?>
<div> content of module c </div></body></html>
在頁面的頭部插入一個html注釋 <!--[CSS LINKS PLACEHOLDER]--> 作為占位,而將原來字面書寫的資源引用改成模板接口 require_static 調用,該接口負責收集頁面所需資源。
requirestatic接口實現非常簡單,就是準備一個數組,收集資源引用,并且可以去重。最后在頁面輸出的前一刻,我們將requirestatic在運行時收集到的 a.css、b.css、c.css 三個資源拼接成html標簽,替換掉注釋占位 <!--[CSS LINKS PLACEHOLDER]-->,從而得到我們需要的頁面結構。
經過實踐總結,可以發現模板層面只要實現三個開發接口,就可以比較完美的實現目前遺留的大部分性能優化原則,這三個接口分別是:
requirestatic(resid):收集資源加載需求的接口,參數是靜態資源id。
loadwidget(wigetid):加載拆分成小組件模板的接口。你可以叫它為widget、component或者pagelet之類的。總之,我們需要一個接口把一個大的頁面模板拆分成一個個的小部分來維護,最后在原來的頁面中以組件為單位來加載這些小部件。
script(code):收集寫在模板中的js腳本,使之出現的頁面底部,從而實現性能優化原則中的 將js放在頁面底部 原則。
實現了這些接口之后,一個重構后的模板頁面的源代碼可能看起來就是這樣的了:
<html><head>
<title>page</title>
<?php require_static('jquery.js'); ?>
<?php require_static('bootstrap.css'); ?>
<?php require_static('bootstrap.js'); ?>
<!--[ CSS LINKS PLACEHOLDER ]--></head><body>
<?php load_widget('a'); ?>
<?php load_widget('b'); ?>
<?php load_widget('c'); ?>
<!--[ SCRIPTS PLACEHOLDER ]--></body></html>
而最終在模板解析的過程中,資源收集與去重、頁面script收集、占位符替換操作,最終從服務端發送出來的html代碼為:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="bootstrap.css"/>
<link rel="stylesheet" type="text/css" href="a.css"/>
<link rel="stylesheet" type="text/css" href="b.css"/>
<link rel="stylesheet" type="text/css" href="c.css"/></head><body>
<div> content of module a </div>
<div> content of module b </div>
<div> content of module c </div>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="bootstrap.js"></script>
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script></body></html>
不難看出,我們目前已經實現了 按需加載,將腳本放在底部,將樣式表放在頭部 三項優化原則。
前面講到靜態資源在上線后需要添加hash戳作為版本標識,那么這種使用模板語言來收集的靜態資源該如何實現這項功能呢?
答案是:靜態資源依賴關系表。
考慮這樣的目錄結構:
project
- widget
- a
- a.css
- a.js
- a.php
- b
- b.css
- b.js
- b.php
- c
- c.css
- c.js
- c.php
- jquery.js
- bootstrap.css
- bootstrap.js
- index.php
如果我們可以使用工具掃描整個project目錄,然后創建一張資源表,同時記錄每個資源的部署路徑,得到這樣的一張表:
{
"res" : {
"widget/a/a.css" : "/widget/a/a_1688c82.css",
"widget/a/a.js" : "/widget/a/a_ac3123s.js",
"widget/b/b.css" : "/widget/b/b_52923ed.css",
"widget/b/b.js" : "/widget/b/b_a5cd123.js",
"widget/c/c.css" : "/widget/c/c_03cab13.css",
"widget/c/c.js" : "/widget/c/c_bf0ae3f.js",
"jquery.js" : "/jquery_9151577.js",
"bootstrap.css" : "/bootstrap_f5ba12d.css",
"bootstrap.js" : "/bootstrap_a0b3ef9.js"
},
"pkg" : {}}
基于這張表,我們就很容易實現 require_static(file_id),load_widget(widget_id) 這兩個模板接口了。以load_widget為例:
function load_widget($id){
//從json文件中讀取資源表
$map = load_map();
//查找靜態資源
$filename = 'widget/' . $id . '/' . $id;
//查找js文件
$js = $filename . '.js';
if(isset($map['res'][$js])) {
//如果有對應的js資源,就收集起來
collect_js_static($map['res'][$js]);
}
//查找css文件
$css = $filename . '.css';
if(isset($map['res'][$css])) {
//如果有對應的css資源,就收集起來
collect_css_static($map['res'][$css]);
}
include $filename . '.php';}
利用查表來解決md5戳的問題,這樣,我們的頁面最終送達給用戶的結果就是這樣的:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="/bootstrap_f5ba12d.css"/>
<link rel="stylesheet" type="text/css" href="/widget/a/a_1688c82.css"/>
<link rel="stylesheet" type="text/css" href="/widget/b/b_52923ed.css"/>
<link rel="stylesheet" type="text/css" href="/widget/c/c_03cab13.css"/></head><body>
<div> content of module a </div>
<div> content of module b </div>
<div> content of module c </div>
<script type="text/javascript" src="/jquery_9151577.js"></script>
<script type="text/javascript" src="/bootstrap_a0b3ef9.js"></script>
<script type="text/javascript" src="/widget/a/a_ac3123s.js"></script>
<script type="text/javascript" src="/widget/b/b_a5cd123.js"></script>
<script type="text/javascript" src="/widget/c/c_bf0ae3f.js"></script></body></html>
接下來,我們討論基于表的設計思想上是如何實現靜態資源合并的。或許有些團隊使用過combo服務,也就是我們在最終拼接生成頁面資源引用的時候,并不是生成多個獨立的link標簽,而是將資源地址拼接成一個url路徑,請求一種線上的動態資源合并服務,從而實現減少HTTP請求的需求,比如前面的例子,稍作調整即可得到這樣的結果:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="/??bootstrap_f5ba12d.css,widget/a/a_1688c82.css,widget/b/b_52923ed.css,widget/c/c_03cab13.css"/></head><body>
<div> content of module a </div>
<div> content of module b </div>
<div> content of module c </div>
<script type="text/javascript" src="/??jquery_9151577.js,bootstrap_a0b3ef9.js,widget/a/a_ac3123s.js,widget/b/b_a5cd123.js,widget/c/c_bf0ae3f.js"></script></body></html>
這個 /??file1,file2,file3,… 的url請求響應就是動態combo服務提供的,它的原理很簡單,就是根據url找到對應的多個文件,合并成一個文件來響應請求,并將其緩存,以加快訪問速度。
這種方法很巧妙,有些服務器甚至直接集成了這類模塊來方便的開啟此項服務,這種做法也是大多數大型web應用的資源合并做法。但它也存在一些缺陷:
瀏覽器有url長度限制,因此不能無限制的合并資源。
如果用戶在網站內有公共資源的兩個頁面間跳轉訪問,由于兩個頁面的combo的url不一樣導致用戶不能利用瀏覽器緩存來加快對公共資源的訪問速度。
如果combo的url中任何一個文件發生改變,都會導致整個url緩存失效,從而導致瀏覽器緩存利用率降低。
對于上述第二條缺陷,可以舉個例子來看說明:
假設網站有兩個頁面A和B
A頁面使用了a,b,c,d四個資源
B頁面使用了a,b,e,f四個資源
如果使用combo服務,我們會得:
A頁面的資源引用為:/??a,b,c,d
B頁面的資源引用為:/??a,b,e,f
兩個頁面引用的資源是不同的url,因此瀏覽器會請求兩個合并后的資源文件,跨頁面訪問沒能很好的利用a、b這兩個資源的緩存。
很明顯,如果combo服務能聰明的知道A頁面使用的資源引用為 /??a,b 和 /??c,d,而B頁面使用的資源引用為 /??a,b 和 /??e,f就好了。這樣當用戶在訪問A頁面之后再訪問B頁面時,只需要下載B頁面的第二個combo文件即可,第一個文件已經在訪問A頁面時緩存好了的。
基于這樣的思考,我們在資源表上新增了一個字段,取名為 pkg,就是資源合并生成的新資源,表的結構會變成:
{
"res" : {
"widget/a/a.css" : "/widget/a/a_1688c82.css",
"widget/a/a.js" : "/widget/a/a_ac3123s.js",
"widget/b/b.css" : "/widget/b/b_52923ed.css",
"widget/b/b.js" : "/widget/b/b_a5cd123.js",
"widget/c/c.css" : "/widget/c/c_03cab13.css",
"widget/c/c.js" : "/widget/c/c_bf0ae3f.js",
"jquery.js" : "/jquery_9151577.js",
"bootstrap.css" : "/bootstrap_f5ba12d.css",
"bootstrap.js" : "/bootstrap_a0b3ef9.js"
},
"pkg" : {
"p0" : {
"url" : "/pkg/lib_cef213d.js",
"has" : [ "jquery.js", "bootstrap.js" ]
},
"p1" : {
"url" : "/pkg/lib_afec33f.css",
"has" : [ "bootstrap.css" ]
},
"p2" : {
"url" : "/pkg/widgets_22feac1.js",
"has" : [
"widget/a/a.js",
"widget/b/b.js",
"widget/c/c.js"
]
},
"p3" : {
"url" : "/pkg/widgets_af23ce5.css",
"has" : [
"widget/a/a.css",
"widget/b/b.css",
"widget/c/c.css"
]
}
}}
相比之前的表,可以看到新表中多了一個pkg字段,并且記錄了打包后的文件所包含的獨立資源。這樣,我們重新設計一下 requirestatic、loadwidget 這兩個模板接口,實現這樣的邏輯:
在查表的時候,如果一個靜態資源有pkg字段,那么就去加載pkg字段所指向的打包文件,否則加載資源本身。
比如執行require_static('bootstrap.js'),查表得知bootstrap.js被打包在了p1中,因此取出p1包的url /pkg/lib_cef213d.js,并且記錄頁面已加載了 jquery.js 和 bootstrap.js 兩個資源。這樣一來,之前的模板代碼執行之后得到的html就變成了:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="/pkg/lib_afec33f.css"/>
<link rel="stylesheet" type="text/css" href="/pkg/widgets_af23ce5.css"/></head><body>
<div> content of module a </div>
<div> content of module b </div>
<div> content of module c </div>
<script type="text/javascript" src="/pkg/lib_cef213d.js"></script>
<script type="text/javascript" src="/pkg/widgets_22feac1.js"></script></body></html>
雖然這種策略請求有4個,不如combo形式的請求少,但可能在統計上是性能更好的方案。由于兩個lib打包的文件修改的可能性很小,因此這兩個請求的緩存利用率會非常高,每次項目發布后,用戶需要重新下載的靜態資源可能要比combo請求節省很多帶寬。
性能優化既是一個工程問題,又是一個統計問題。優化性能時如果只關注一個頁面的首次加載是很片面的。還應該考慮全站頁面間跳轉、項目迭代后更新資源等情況下的優化策略。
此時,我們又引入了一個新的問題:如何決定哪些文件被打包?
從經驗來看,項目初期可以采用人工配置的方式來指定打包情況,比如:
{
"pack" : {
"lib.js" : [ "jquery.js", "bootstrap.js" ],
"lib.css" : "bootstrap.css",
"widgets.js" : "widget/**.js",
"widgets.css" : "widget/**.css"
}}
但隨著系統規模的增大,人工配置會帶來非常高的維護成本,此時需要一個輔助系統,通過分析線上訪問日志和靜態資源組合加載情況來自動生成這份配置文件,系統設計如圖:
至此,我們通過基于表的靜態資源管理系統和三個模板接口實現了幾個重要的性能優化原則,現在我們再來回顧一下前面的性能優化原則分類表,剔除掉已經做到了的,看看還剩下哪些沒做到的:
優化方向 優化手段
請求數量 拆分初始化負載
緩存利用 使Ajax可緩存
頁面結構 盡早刷新文檔的輸出
拆分初始化負載 的目標是將頁面一開始加載時不需要執行的資源從所有資源中分離出來,等到需要的時候再加載。工程師通常沒有耐心去區分資源的分類情況,但我們可以利用組件化框架接口來幫助工程師管理資源的使用。還是從例子開始思考,如果我們有一個js文件是用戶交互后才需要加載的,會怎樣呢:
<html><head>
<title>page</title>
<?php require_static('jquery.js'); ?>
<?php require_static('bootstrap.css'); ?>
<?php require_static('bootstrap.js'); ?>
<!--[ CSS LINKS PLACEHOLDER ]--></head><body>
<?php load_widget('a'); ?>
<?php load_widget('b'); ?>
<?php load_widget('c'); ?>
<?php script('start'); ?>
<script>
$(document.body).click(function(){
require.async('dialog.js', function(dialog){
dialog.show('you catch me!');
});
});
</script>
<?php script('end'); ?>
<!--[ SCRIPTS PLACEHOLDER ]--></body></html>
很明顯,dialog.js 這個文件我們不需要在初始化的時候就加載,因此它應該在后續的交互中再加載,但文件都加了md5戳,我們如何能在瀏覽器環境中知道加載的url呢?
答案就是:把靜態資源表的一部分輸出在頁面上,供前端模塊化框架加載靜態資源。
我就不多解釋代碼的執行過程了,大家看到完整的html輸出就能理解是怎么回事了:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="/pkg/lib_afec33f.css"/>
<link rel="stylesheet" type="text/css" href="/pkg/widgets_af23ce5.css"/></head><body>
<div> content of module a </div>
<div> content of module b </div>
<div> content of module c </div>
<script type="text/javascript" src="/pkg/lib_cef213d.js"></script>
<script type="text/javascript" src="/pkg/widgets_22feac1.js"></script>
<script>
//將靜態資源表輸出在前端頁面中
require.config({
res : {
'dialog.js' : '/dialog_fa3df03.js'
}
});
</script>
<script>
$(document.body).click(function(){
//require.async接口查表確定加載資源的url
require.async('dialog.js', function(dialog){
dialog.show('you catch me!');
});
});
</script></body></html>
dialog.js不會在頁面以script src的形式輸出,而是變成了資源注冊,這樣,當頁面點擊觸發require.async執行的時候,async函數才會查表找到資源的url并加載它,加載完畢后觸發回調函數。
以上框架示例我實現了一個java-jsp版的,有興趣的同學請看這里:https://github.com/fouber/fis-java-jsp
到目前為止,我們又以架構的形式實現了一項優化原則(拆分初始化負載),回顧我們的優化分類表,現在僅有兩項沒能做到了:
優化方向 優化手段
緩存利用 使Ajax可緩存
頁面結構 盡早刷新文檔的輸出
剩下的兩項優化原則要做到并不容易,真正可緩存的Ajax在現實開發中比較少見,而 盡早刷新文檔的輸出 原則facebook在2010年的velocity上 提到過,就是BigPipe技術。當時facebook團隊還講到了Quickling和PageCache兩項技術,其中的PageCache算是比較徹底的實現Ajax可緩存的優化原則了。由于篇幅關系,就不在此展開了,后續還會撰文詳細解讀這兩項技術。
總結
其實在前端開發工程管理領域還有很多細節值得探索和挖掘,提升前端團隊生產力水平并不是一句空話,它需要我們能對前端開發及代碼運行有更深刻的認識,對性能優化原則有更細致的分析與研究。在前端工業化開發的所有環節均有可節省的人力成本,這些成本非常可觀,相信現在很多大型互聯網公司也都有了這樣的共識。
本文只是將這個領域中很小的一部分知識的展開討論,拋磚引玉,希望能為業界相關領域的工作者提供一些不一樣的思路。
來源:http://div.io/topic/371
- PHP技術文章
- PHP中session和cookie的區別
- php設計模式(一):簡介及創建型模式
- php設計模式結構型模式
- Php設計模式(三):行為型模式
- 十款最出色的 PHP 安全開發庫中文詳細介紹
- 12個提問頻率最高的PHP面試題
- PHP 語言需要避免的 10 大誤區
- PHP 死鎖問題分析
- 致PHP路上的“年輕人”
- PHP網站常見安全漏洞,及相應防范措施總結
- 各開源框架使用與設計總結(一)
- 數據庫的本質、概念及其應用實踐(二)
- PHP導出MySQL數據到Excel文件(fputcsv)
- PHP中14種排序算法評測
- 深入理解PHP原理之--echo的實現
- PHP性能分析相關的函數
- PHP 性能分析10則
- 10 位頂級 PHP 大師的開發原則
- 30條爆笑的程序員梗 PHP是最好的語言
- PHP底層的運行機制與原理
- PHP 性能分析與實驗——性能的宏觀分析
- PHP7 性能翻倍關鍵大揭露
- 鳥哥:寫在PHP7發布之際一些話
- PHP與MySQL通訊那點事
- Php session內部執行流程的再次剖析
- 關于 PHP 中的 Class 的幾點個人看法
- PHP Socket 編程過程詳解
- PHP過往及現在及變革
- PHP吉祥物大象的由來
- PHP生成靜態頁面的方法
- 吊炸天的 PHP 7 ,你值得擁有!
- PHP開發中文件操作疑難問答
- MongoDB PHP Driver的連接處理解析
- PHP 雜談《重構-改善既有代碼的設計》之二 對象
- 在php中判斷一個請求是ajax請求還是普通請求的方法
- 使用HAProxy、PHP、Redis和MySQL支撐10億請求每周架構細節
- HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、Web Services 是什么?
- 重構-改善既有代碼的設計
- PHP場景中getshell防御思路分享
- 移動互聯時代,你看看除了PHP你還會些什么
- 安卓系統上搭建本地php服務器環境
- PHP中常見的緩存技術!
- PHP里10個鮮為人知但卻非常有用的函數
- 成為一名PHP專家其實并不難
- PHP 命令行?是的,您可以!
- PHP開發提高效率技巧
- PHP八大安全函數解析
- PHP實現四種基本排序算法
- PHP開發中的中文編碼問題
- php.get.post
- php發送get、post請求的6種方法簡明總結
- 中高級PHP開發者應該掌握哪些技術?
- 前端開發
- web前端知識體系大全
- 前端工程與性能優化(下)
- 前端工程與性能優化(上)
- 2016 年技術發展方向
- Web應用檢查清單
- 如何成為一名優秀的web前端工程師
- 前端組件化開發實踐
- 移動端H5頁面高清多屏適配方案
- 2015前端框架何去何從
- 從前端看“百度遷徙”的技術實現(一)
- 從前端看“百度遷徙”的技術實現(二)
- 前端路上的旅行
- 大公司里怎樣開發和部署前端代碼?
- 5個經典的前端面試問題
- 前端工程師新手必讀
- 手機淘寶前端的圖片相關工作流程梳理
- 一個自動化的前端項目實現(附源碼)
- 前端代碼異常日志收集與監控
- 15年雙11手淘前端技術總結 - H5性能最佳實踐
- 深入理解javascript原型和閉包系列
- 一切都是對象
- 函數和對象的關系
- prototype原型
- 隱式原型
- instanceof
- 繼承
- 原型的靈活性
- 簡述【執行上下文】上
- 簡述【執行上下文】下
- this
- 執行上下文棧
- 簡介【作用域】
- 【作用域】和【上下文環境】
- 從【自由變量】到【作用域鏈】
- 閉包
- 完結
- 補充:上下文環境和作用域的關系
- Linux私房菜