在一個(gè)討論web技術(shù)的網(wǎng)站vitamin上發(fā)現(xiàn)這篇《Serving JavaScript Fast》,讀過之后大有收獲,茅塞頓開。于是就有了翻譯過來的念頭——我這人有個(gè)毛病,看到有意思的英文文章,就想自己翻過來(雖然英文水平很爛)。先在網(wǎng)上查了查,已經(jīng)有blog談到這篇文章(我算是后知后覺了),有總結(jié)要點(diǎn)的《Flickr 的開發(fā)者的 Web 應(yīng)用優(yōu)化技巧》,也有延伸開來的《接著講Flickr的八卦》,但似乎沒有全文翻譯的(這下就好,不會(huì)忙了半天發(fā)現(xiàn)是無用功)。之后,就寫信問作者可不可以,作者一口答應(yīng):“sure - i’d love you to translate it”,只是要求我翻好之后給他一個(gè)鏈接地址。得到準(zhǔn)許,心里就有底了。
先介紹一下作者。Cal Henderson,倫敦人,現(xiàn)居加利福尼亞的舊金山。PHP,MySQL和Perl專家,現(xiàn)任flickr架構(gòu)師(flickr被收購后就在yahoo了),同時(shí)也是vitamin的特聘顧問(寫些技術(shù)性文章)。
既然他是架構(gòu)師,flickr用的應(yīng)該就是文中談到的這些技術(shù),于是參照文章,再對(duì)比網(wǎng)站,種種跡象表明確實(shí)如此。雖然在中國訪問flickr速度不敢恭維,加速效果不得而知,但其用了n多css和javascript資源卻似乎從沒出過什么問題,也從側(cè)面印證了這些技術(shù)的有效性。
仔細(xì)的看完文章,還有個(gè)強(qiáng)烈的感覺:這老兄也太能賣關(guān)子了,一句話非分成三句說,擺事實(shí)講道理是夠透徹,就是有點(diǎn)太@#$%了…… 算了,他怎么說我怎么翻吧,忠實(shí)于原著嘛,要不就成篡改了。經(jīng)過幾天努力,加上同事thincat兄傾力援手(小弟不勝感激?。?,終于完工(@_@ 真是苦力活啊,我再也不想干了~)。
全文翻譯如下:
作者:Cal Henderson
下一代web應(yīng)用讓javascript和css得堪大用。我們會(huì)告訴你怎樣使這些應(yīng)用又快又靈。
建立了號(hào)稱“Web 2.0”的應(yīng)用,也實(shí)現(xiàn)了富內(nèi)容(rich content)和交互,我們期待著css和javascript扮演更加重要的角色。為使應(yīng)用干凈利落,我們需要完善那些渲染頁面的文件,優(yōu)化其大小和形態(tài),以確保提供最好的用戶體驗(yàn)——在實(shí)踐中,這就意味著一種結(jié)合:使內(nèi)容盡可能小、下載盡可能快,同時(shí)避免對(duì)未改動(dòng)資源不必要的重新獲取。
由于css和js文件的形態(tài),情況有點(diǎn)復(fù)雜。跟圖片相比,其源代碼很有可能頻繁改動(dòng)。而一旦改動(dòng),就需要客戶端重新下載,使本地緩存無效(保存在其他緩存里的版本也是如此)。在這篇文章里,我們將著重探討怎樣使用戶體驗(yàn)最快:包括初始頁面的下載,隨后頁面的下載,以及隨著應(yīng)用漸進(jìn)、內(nèi)容變化而進(jìn)行的資源下載。
我始終堅(jiān)信這一點(diǎn):對(duì)開發(fā)者來說,應(yīng)該盡可能讓事情變得簡(jiǎn)單。所以我們青睞于那些能讓系統(tǒng)自動(dòng)處理優(yōu)化難題的方法。只需少許工作量,我們就能建立一舉多得的環(huán)境:它使開發(fā)變得簡(jiǎn)單,有極佳的終端性能,也不會(huì)改變現(xiàn)有的工作方式。
老的思路是,為優(yōu)化性能,可以把多個(gè)css和js文件合并成極少數(shù)大文件。跟十個(gè)5k的js文件相比,合并成一個(gè)50k的文件更好。雖然代碼總字節(jié)數(shù)沒變,卻避免了多個(gè)HTTP請(qǐng)求造成的開銷。每個(gè)請(qǐng)求都會(huì)在客戶端和服務(wù)器兩邊有個(gè)建立和消除的過程,導(dǎo)致請(qǐng)求和響應(yīng)header帶來開銷,還有服務(wù)器端更多的進(jìn)程和線程資源消耗(可能還有為壓縮內(nèi)容耗費(fèi)的cpu時(shí)間)。
(除了HTTP請(qǐng)求,)并發(fā)問題也很重要。默認(rèn)情況下,在使用持久連接(persistent connections)時(shí),ie和firefox在同一域名內(nèi)只會(huì)同時(shí)下載兩個(gè)資源(在HTTP 1.1規(guī)格書中第8.1.4節(jié)的建議)(htmlor注:可以通過修改注冊(cè)表等方法改變這一默認(rèn)配置)。這就意味著,在我們等待下載2個(gè)js文件的同時(shí),將無法下載圖片資源。也就是說,這段時(shí)間內(nèi)用戶在頁面上看不到圖片。
(雖然合并文件能解決以上兩個(gè)問題,)可是,這個(gè)方法有兩個(gè)缺點(diǎn)。第一,把所有資源一起打包,將強(qiáng)制用戶一次下載完所有資源。如果(不這么做,而是)把大塊內(nèi)容變成多個(gè)文件,下載開銷就分散到了多個(gè)頁面,同時(shí)緩解了會(huì)話中的速度壓力(或完全避免了某些開銷,這取決于用戶選擇的路徑)。如果為了隨后頁面下載得更快而讓初始頁面下載得很慢,我們將發(fā)現(xiàn)更多用戶根本不會(huì)傻等著再去打開下一個(gè)頁面。
第二(這個(gè)影響更大,一直以來卻沒怎么被考慮過),在一個(gè)文件改動(dòng)很頻繁的環(huán)境里,如果采用單文件系統(tǒng),那么每次改動(dòng)文件都需要客戶端把所有css和js重新下載一遍。假如我們的應(yīng)用有個(gè)100k的合成的js大文件,任何微小的改動(dòng)都將強(qiáng)制客戶端把這100k再消化一遍。
(看來合并成大文件不太合適。)替代方案是個(gè)折中的辦法:把css和js資源分散成多個(gè)子文件,按功能劃分、保持文件個(gè)數(shù)盡可能少。這個(gè)方案也是有代價(jià)的,雖說開發(fā)時(shí)代碼分散成邏輯塊(logical chunks)能提高效率,可在下載時(shí)為提高性能還得合并文件。不過,只要給build系統(tǒng)(把開發(fā)代碼變成產(chǎn)品代碼的工具集,是為部署準(zhǔn)備的)加點(diǎn)東西,就沒什么問題了。
對(duì)于有著不同開發(fā)和產(chǎn)品環(huán)境的應(yīng)用來說,用些簡(jiǎn)單的技術(shù)可以讓代碼更好管理。在開發(fā)環(huán)境下,為使條理清晰,代碼可以分散為多個(gè)邏輯部分(logical components)??梢栽?a >Smarty(一種php模板語言)里建立一個(gè)簡(jiǎn)單的函數(shù)來管理javascript的下載:
SMARTY:{insert_js files="foo.js,bar.js,baz.js"}PHP:function smarty_insert_js($args){ foreach (explode(‘,‘, $args[‘files‘]) as $file){ echo "<script type=\"text/javascript\" SOURCE=\"/javascript/$file\"></script>\n"; }}OUTPUT:<script type="text/javascript" SOURCE="/javascript/foo.js"></script><script type="text/javascript" SOURCE="/javascript/bar.js"></script><script type="text/javascript" SOURCE="/javascript/baz.js"></script>
(htmlor注:wordpress中會(huì)把“src”替換成不知所謂的字符,因此這里只有寫成“SOURCE”,使用代碼時(shí)請(qǐng)注意替換,下同)
就這么簡(jiǎn)單。然后我們就命令build過程(build process)去把確定的文件合并起來。這個(gè)例子里,合并的是foo.js和bar.js,因?yàn)樗鼈儙缀蹩偸且黄鹣螺d。我們能讓應(yīng)用配置記住這一點(diǎn),并修改模板函數(shù)去使用它。(代碼如下:)
SMARTY:{insert_js files="foo.js,bar.js,baz.js"}PHP:# 源文件映射圖。在build過程合并文件之后用這個(gè)圖找到j(luò)s的源文件。$GLOBALS[‘config‘][‘js_source_map‘] = array( ‘foo.js‘ => ‘foobar.js‘, ‘bar.js‘ => ‘foobar.js‘, ‘baz.js‘ => ‘baz.js‘,);function smarty_insert_js($args){ if ($GLOBALS[‘config‘][‘is_dev_site‘]){ $files = explode(‘,‘, $args[‘files‘]); }else{ $files = array(); foreach (explode(‘,‘, $args[‘files‘]) as $file){ $files[$GLOBALS[‘config‘][‘js_source_map‘][$file]]++; } $files = array_keys($files); } foreach ($files as $file){ echo "<script type=\"text/javascript\" SOURCE=\"/javascript/$file\"></script>\n"; }}OUTPUT:<script type="text/javascript" SOURCE="/javascript/foobar.js"></script><script type="text/javascript" SOURCE="/javascript/baz.js"></script>
模板里的源代碼沒必要為了分別適應(yīng)開發(fā)和產(chǎn)品階段而改動(dòng),它幫助我們?cè)陂_發(fā)時(shí)保持文件分散,發(fā)布成產(chǎn)品時(shí)把文件合并。想更進(jìn)一步的話,可以把合并過程(merge process)寫在php里,然后使用同一個(gè)(合并文件的)配置去執(zhí)行。這樣就只有一個(gè)配置文件,避免了同步問題。為了做的更加完美,我們還可以分析css和js文件在頁面中同時(shí)出現(xiàn)的幾率,以此決定合并哪些文件最合理(幾乎總是同時(shí)出現(xiàn)的文件是合并的首選)。
對(duì)css來說,可以先建立一個(gè)主從關(guān)系的模型,它很有用。一個(gè)主樣式表控制應(yīng)用的所有樣式表,多個(gè)子樣式表控制不同的應(yīng)用區(qū)域。采用這個(gè)方法,大多數(shù)頁面只需下載兩個(gè)css文件,而其中一個(gè)(指主樣式表)在頁面第一次請(qǐng)求時(shí)就會(huì)緩存。
對(duì)沒有太多css和js資源的應(yīng)用來說,這個(gè)方法在第一次請(qǐng)求時(shí)可能比單個(gè)大文件慢,但如果保持文件數(shù)量很少的話,你會(huì)發(fā)現(xiàn)其實(shí)它更快,因?yàn)槊總€(gè)頁面的數(shù)據(jù)量更小。讓人頭疼的下載花銷被分散到不同的應(yīng)用區(qū)域,因此并發(fā)下載數(shù)保持在一個(gè)最小值,同時(shí)也使得頁面的平均下載數(shù)據(jù)量很小。
談到資源壓縮,大多數(shù)人馬上會(huì)想到mod_gzip(但要當(dāng)心,mod_gzip實(shí)際上是個(gè)魔鬼,至少能讓人做惡夢(mèng))。它的原理很簡(jiǎn)單:瀏覽器請(qǐng)求資源時(shí),會(huì)發(fā)送一個(gè)header表明自己能接受的內(nèi)容編碼。就像這樣:
Accept-Encoding: gzip,deflate
服務(wù)器遇到這樣的header請(qǐng)求時(shí),就用gzip或deflate壓縮內(nèi)容發(fā)往客戶端,然后客戶端解壓縮。這過程減少了數(shù)據(jù)傳輸量,同時(shí)消耗了客戶端和服務(wù)器的cpu時(shí)間。也算差強(qiáng)人意。但是,mod_gzip的工作方式是這樣的:先在磁盤上創(chuàng)建一個(gè)臨時(shí)文件,然后發(fā)送(給客戶端),最后刪除這個(gè)文件。在高容量的系統(tǒng)中,由于磁盤io問題,很快就會(huì)達(dá)到極限。要避免這種情況,可以改用mod_deflate(apache 2才支持)。它采用更合理的方式:在內(nèi)存里做壓縮。對(duì)于apache 1的用戶來說,可以建立一塊ram磁盤,讓mod_gzip在它上面寫臨時(shí)文件。雖然沒有純內(nèi)存方式快,但也不會(huì)比往磁盤上寫文件慢。
話雖如此,其實(shí)還是有辦法完全避免壓縮開銷的,那就是預(yù)壓縮相關(guān)靜態(tài)資源,下載時(shí)由mod_gzip提供合適的壓縮版本。如果把壓縮添加在build過程,它就很透明了。需要壓縮的文件通常很少(用不著壓縮圖片,因?yàn)椴⒉荒軠p小更多體積),只有css和js文件(和其他未壓縮的靜態(tài)內(nèi)容)。
配置選項(xiàng)會(huì)告訴mod_gzip去哪里找到預(yù)壓縮過的文件。
mod_gzip_can_negotiate Yesmod_gzip_static_suffix .gzAddEncoding gzip .gz
新一點(diǎn)的mod_gzip版本(從1.3.26.1a開始)添加一個(gè)額外的配置選項(xiàng)后,就能自動(dòng)預(yù)壓縮文件。不過在此之前,必須確認(rèn)apache有正確的權(quán)限去創(chuàng)建和覆蓋壓縮文件。
mod_gzip_update_static Yes
可惜,事情沒那么簡(jiǎn)單。某些Netscape 4的版本(尤其是4.06-4.08)認(rèn)為自己能夠解釋壓縮內(nèi)容(它們發(fā)送一個(gè)header這么說來著),但其實(shí)它們不能正確的解壓縮。大多數(shù)其他版本的Netscape 4在下載壓縮內(nèi)容時(shí)也有各種各樣的問題。所以要在服務(wù)器端探測(cè)代理類型,(如果是Netscape 4,就要)讓它們得到未壓縮的版本。這還算簡(jiǎn)單的。ie(版本4-6)有些更有意思的問題:當(dāng)下載壓縮的javascript時(shí),有時(shí)候ie會(huì)不正確的解壓縮文件,或者解壓縮到一半中斷,然后把這半個(gè)文件顯示在客戶端。如果你的應(yīng)用對(duì)javascript的依賴比較大(htmlor注:比如ajax應(yīng)用),那么就得避免發(fā)送壓縮文件給ie。在某些情況下,一些更老的5.x版本的ie倒是能正確的收到壓縮的javascript,可它們會(huì)忽略這個(gè)文件的etag header,不緩存它。(thincat友情提示:盡管壓縮存在一些瀏覽器不兼容的現(xiàn)象,由于這些不能很好的支持壓縮的瀏覽器數(shù)量現(xiàn)在已經(jīng)非常少了,我認(rèn)為這種由于瀏覽器導(dǎo)致的壓縮不正常的情況可以忽略不計(jì)。這些過時(shí)的瀏覽器還能不能在現(xiàn)在流行的windows或unix環(huán)境下面安裝都存在不小的問題)
既然gzip壓縮有這么多問題,我們不妨把注意力轉(zhuǎn)到另一邊:不改變文件格式的壓縮?,F(xiàn)在有很多這樣的javascript壓縮腳本可用,大多數(shù)都用一個(gè)正則表達(dá)式驅(qū)動(dòng)的語句集來減小源代碼的體積。它們做的不外乎幾件事:去掉注釋,壓縮空格,縮短私有變量名和去掉可省略的語法。
不幸的是,大多數(shù)腳本效果并不理想,要么壓縮率相當(dāng)?shù)?,要么某種情形下會(huì)把代碼搞得一團(tuán)糟(或者兩者兼而有之)。由于對(duì)解析樹的理解不完整,壓縮器很難區(qū)分一句注釋和一句看似注釋的引用字符串。因?yàn)殚]合結(jié)構(gòu)的混合使用,要用正則表達(dá)式發(fā)現(xiàn)哪些變量是私有的并不容易,因此一些縮短變量名的技術(shù)會(huì)打亂某些閉合代碼。
還好有個(gè)壓縮器能避免這些問題:dojo壓縮器(現(xiàn)成的版本在這里)。它使用rhino(mozilla的javascript引擎,是用java實(shí)現(xiàn)的)建立一個(gè)解析樹,然后將其提交給文件。它能很好的減小代碼體積,僅用很小的成本:因?yàn)橹辉赽uild時(shí)壓縮一次。由于壓縮是在build過程中實(shí)現(xiàn)的,所以一清二楚。(既然壓縮沒有問題了,)我們可以在源代碼里隨心所欲的添加空格和注釋,而不必?fù)?dān)心影響到產(chǎn)品代碼。
與javascript相比,css文件的壓縮相對(duì)簡(jiǎn)單一些。由于css語法里不會(huì)有太多引用字符串(通常是url路徑跟字體名),我們可以用正則表達(dá)式大刀闊斧的干掉空格(htmlor注:這句翻的最爽,哈哈)。如果確實(shí)有引用字符串的話,我們總可以把一串空格合成一個(gè)(因?yàn)椴恍枰趗rl路徑和字體名里查找多個(gè)空格和tab)。這樣的話,一個(gè)簡(jiǎn)單的perl腳本就夠了:
#!/usr/bin/perlmy $data = ‘‘;open F, $ARGV[0] or die "Can‘t open source file: $!";$data .= $_ while <F>;close F;$data =~ s!/*(.*?)*/!!g; # 去掉注釋$data =~ s!s+! !g; # 壓縮空格$data =~ s!} !}\n!g; # 在結(jié)束大括號(hào)后添加換行$data =~ s!\n$!!; # 刪除最后一個(gè)換行$data =~ s! { ! {!g; # 去除開始大括號(hào)后的空格$data =~ s!; }!}!g; # 去除結(jié)束大括號(hào)前的空格print $data;
然后,就可以把單個(gè)的css文件傳給腳本去壓縮了。命令如下:
perl compress.pl site.source.css > site.compress.css
做完這些簡(jiǎn)單的純文本優(yōu)化工作后,我們就能減少數(shù)據(jù)傳輸量多達(dá)50%了(這個(gè)量取決于你的代碼格式,可能更多)。這帶來了更快的用戶體驗(yàn)。不過我們真正想做的是,盡可能避免用戶請(qǐng)求的發(fā)生——除非確實(shí)有必要。這下HTTP緩存知識(shí)派上用場(chǎng)了。
當(dāng)用戶代理(如瀏覽器)向服務(wù)器請(qǐng)求一個(gè)資源時(shí),第一次請(qǐng)求過后它就會(huì)緩存服務(wù)器的響應(yīng),以避免重復(fù)之后的相同請(qǐng)求。緩存時(shí)間的長短取決于兩個(gè)因素:代理的配置和服務(wù)器的緩存控制header。所有瀏覽器都有不同的配置選項(xiàng)和處理方式,但大多數(shù)都會(huì)把一個(gè)資源至少緩存到會(huì)話結(jié)束(除非被明確告知)。
為了不讓瀏覽器緩存改動(dòng)頻繁的頁面,你很可能已經(jīng)發(fā)送過header不緩存動(dòng)態(tài)內(nèi)容。在php中,以下兩行命令可以做到:
<?phpheader("Cache-Control: private");header("Cache-Control: no-cache", false);?>
聽起來太簡(jiǎn)單了?確實(shí)如此——因?yàn)橛行┐恚g覽器)在某些環(huán)境下將忽略這些header。要確保瀏覽器不緩存文檔,應(yīng)該更強(qiáng)硬一些:
<?php# 讓它在過去就“失效”header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");# 永遠(yuǎn)是改動(dòng)過的header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");# HTTP/1.1header("Cache-Control: no-store, no-cache, must-revalidate");header("Cache-Control: post-check=0, pre-check=0", false);# HTTP/1.0header("Pragma: no-cache");?>
這樣,對(duì)于我們不想緩存的內(nèi)容來說已經(jīng)行了。但對(duì)于那些不會(huì)每次請(qǐng)求時(shí)都有改動(dòng)的內(nèi)容,應(yīng)該鼓勵(lì)瀏覽器更霸道的緩存它?!癐f-Modified-Since”請(qǐng)求header能夠做到這點(diǎn)。如果客戶端在請(qǐng)求中發(fā)送一個(gè)“If-Modified-Since”header,apache(或其他服務(wù)器)會(huì)以狀態(tài)代碼304(沒改過)響應(yīng),告訴瀏覽器緩存已經(jīng)是最新的。使用這個(gè)機(jī)制,能夠避免重復(fù)發(fā)送文件給瀏覽器,不過仍然導(dǎo)致了一個(gè)HTTP請(qǐng)求的消耗。嗯,再想想。
與If-Modified-Since機(jī)制類似的是實(shí)體標(biāo)記(entity tags)。在apache環(huán)境下,每個(gè)對(duì)靜態(tài)文件的響應(yīng)都會(huì)發(fā)出一個(gè)“ETag”header,它包含了一個(gè)由文件修改時(shí)間、文件大小和inode號(hào)生成的校驗(yàn)和(checksum)。在下載文件之前,瀏覽器會(huì)發(fā)送一個(gè)HEAD請(qǐng)求去檢查文件的etag??蒃Tag跟If-Modified-Since有同樣的問題:客戶端仍舊需要執(zhí)行HTTP請(qǐng)求來驗(yàn)證本地緩存是否有效。
此外,如果你使用多臺(tái)服務(wù)器提供內(nèi)容,得小心使用if-modified-since和etags。在兩臺(tái)負(fù)載平衡的服務(wù)器環(huán)境下,對(duì)一個(gè)代理(瀏覽器)來說,一個(gè)資源可以這次從A服務(wù)器得到,下次從B服務(wù)器得到(htmlor注:lvs負(fù)載平衡系統(tǒng)就是個(gè)典型的例子)。這很好,也是采用平衡負(fù)載的原因??墒牵绻麅膳_(tái)服務(wù)器給同一個(gè)文件生成了不同的etag或者文件修改日期,瀏覽器就無所適從了(每次都會(huì)重新下載)。默認(rèn)情況下,etag是由文件的inode號(hào)生成的,而多臺(tái)服務(wù)器之間文件的inode號(hào)是不同的??梢允褂胊pache的配置選項(xiàng)關(guān)掉它:
FileETag MTime Size
使用這個(gè)選項(xiàng),apache將只用文件修改日期和文件大小來決定etag。很不幸,這導(dǎo)致了另一個(gè)問題(一樣能影響if-modified-since)。既然etag依賴于修改時(shí)間,就得讓時(shí)間同步??赏嗯_(tái)服務(wù)器上傳文件時(shí),上傳時(shí)間差個(gè)一到兩秒是常有的事。這樣一來,兩臺(tái)服務(wù)器生成的etag還是不一樣。當(dāng)然,我們還可以改變配置,讓etag的生成只取決于文件大小,但這就意味著如果文件內(nèi)容變了而大小沒變,etag也不會(huì)變。這可不行。
看來我們正從錯(cuò)誤的方向入手解決問題。(現(xiàn)在的問題是,)這些可能的緩存策略導(dǎo)致了一件事情反復(fù)發(fā)生,那就是:客戶端向服務(wù)器查詢本地緩存是否最新。假如服務(wù)器在改動(dòng)文件的時(shí)候通知客戶端,客戶端不就知道它的緩存是最新的了(直到接到下一次通知)?可惜天公不做美——(事實(shí))是客戶端向服務(wù)器發(fā)出請(qǐng)求。
其實(shí),也不盡然。在獲取js或css文件之前,客戶端會(huì)用<script>或<link>標(biāo)記向服務(wù)器發(fā)送一個(gè)請(qǐng)求,說明哪個(gè)頁面要加載這些文件。這時(shí)候就可以用服務(wù)器的響應(yīng)來通知客戶端這些文件有了改動(dòng)。有點(diǎn)含糊,說得再詳細(xì)點(diǎn)就是:如果改變css和js文件內(nèi)容的同時(shí),也改變它們的文件名,就可以告訴客戶端對(duì)url全都永久緩存——因?yàn)槊總€(gè)url都是唯一的。
假如能確定一個(gè)資源永不更改,我們就可以發(fā)出一些霸氣十足的緩存header(htmlor注:這句也很有氣勢(shì)吧)。在php里,兩行就好:
<?phpheader("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");header("Cache-Control: max-age=315360000");?>
我們告訴瀏覽器這個(gè)內(nèi)容在10年后(10年大概會(huì)有315,360,000秒,或多或少)過期,瀏覽器將會(huì)保留它10年。當(dāng)然,很有可能不用php輸出css和js文件(因此就不能發(fā)出header),這種情況將在稍后說明。
當(dāng)文件內(nèi)容更改時(shí),手動(dòng)去改文件名是很危險(xiǎn)的。假如你改了文件名,模板卻沒有指向它?假如你改了一些模板另一些卻沒改?假如你改了模板卻沒改文件名?還有最糟的,假如你改動(dòng)了文件卻忘了改名或者忘了改變對(duì)它的引用?最好的結(jié)果,是用戶看到老的而看不到新的內(nèi)容。最壞的結(jié)果,是找不到文件,網(wǎng)站沒法運(yùn)轉(zhuǎn)了。聽起來這(指改動(dòng)文件內(nèi)容時(shí)修改url)似乎是個(gè)餿主意。
幸運(yùn)的是,計(jì)算機(jī)做這類事情——當(dāng)某種變化發(fā)生,需要相當(dāng)準(zhǔn)確地完成的、重復(fù)重復(fù)再重復(fù)的(htmlor注:番茄雞蛋伺候~)、枯燥乏味的工作——總是十分在行。
這個(gè)過程(改變文件的url)沒那么痛苦,因?yàn)槲覀兏静恍枰奈募YY源的url和磁盤上文件的位置也沒必要保持一致。使用apache的mod_rewrite模塊,可以建立簡(jiǎn)單的規(guī)則,讓確定的url重定向到確定的文件。
RewriteEngine onRewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L]
這條規(guī)則匹配任何帶有指定擴(kuò)展名同時(shí)含有“版本”信息(version nugget)的url,它會(huì)把這些url重定向到一個(gè)不含版本信息的路徑。如下所示:
URL Path/images/foo.v2.gif -> /images/foo.gif/css/main.v1.27.css -> /css/main.css/javascript/md5.v6.js -> /javascript/md5.js
使用這條規(guī)則,就可以做到不改變文件路徑而更改url(因?yàn)榘姹咎?hào)變了)。由于url變了,瀏覽器就認(rèn)為它是另一個(gè)資源(會(huì)重新下載)。想更進(jìn)一步的話,可以把我們之前說的腳本編組函數(shù)結(jié)合起來,根據(jù)需要生成一個(gè)帶有版本號(hào)的<script>標(biāo)記列表。
說到這里,你可能會(huì)問我,為什么不在url結(jié)尾加一個(gè)查詢字符串(query string)呢(如/css/main.css?v=4)?根據(jù)HTTP緩存規(guī)格書所說,用戶代理對(duì)含有查詢字符串的url永不緩存。雖然ie跟firefox忽略了這點(diǎn),opera和safari卻沒有——為了確保所有瀏覽器都緩存你的資源,還是不要在url里用查詢字符串的好。
現(xiàn)在不移動(dòng)文件就能更改url了,如果能讓url自動(dòng)更新就更好了。在小型的產(chǎn)品環(huán)境下(如果有大型的產(chǎn)品環(huán)境,就是開發(fā)環(huán)境了),使用模板功能可以很輕易的實(shí)現(xiàn)這點(diǎn)。這里用的是smarty,用其他模板引擎也行。
SMARTY:<link xhref="{version xsrc=‘/css/group.css‘}" rel="stylesheet" type="text/css" />PHP:function smarty_version($args){ $stat = stat($GLOBALS[‘config‘][‘site_root‘].$args[‘src‘]); $version = $stat[‘mtime‘]; echo preg_replace(‘!.([a-z]+?)$!‘, ".v$version.$1", $args[‘src‘]);}OUTPUT:<link xhref="/css/group.v1234567890.css" mce_href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />
對(duì)每個(gè)鏈接到的資源文件,我們得到它在磁盤上的路徑,檢查它的mtime(文件最后修改的日期和時(shí)間),然后把這個(gè)時(shí)間當(dāng)作版本號(hào)插入到url中。對(duì)于低流量的站點(diǎn)(它們的stat操作開銷不大)或者開發(fā)環(huán)境來說,這個(gè)方案不錯(cuò),但對(duì)于高容量的環(huán)境就不適用了——因?yàn)槊看蝧tat操作都要磁盤讀?。▽?dǎo)致服務(wù)器負(fù)載升高)。
解決方案相當(dāng)簡(jiǎn)單。在大型系統(tǒng)中每個(gè)資源都已經(jīng)有了一個(gè)版本號(hào),就是版本控制的修訂號(hào)(你們應(yīng)該使用了版本控制,對(duì)吧?)。當(dāng)我們建立站點(diǎn)準(zhǔn)備部署的時(shí)候,可以輕易的查到每個(gè)文件的修訂號(hào),寫在一個(gè)靜態(tài)配置文件里。
<?php$GLOBALS[‘config‘][‘resource_versions‘] = array( ‘/images/foo.gif‘ => ‘2.1‘, ‘/css/main.css‘ => ‘1.27‘, ‘/javascript/md5.js‘ => ‘6.1.4‘,);?>
當(dāng)我們發(fā)布產(chǎn)品時(shí),可以修改模板函數(shù)來使用版本號(hào)。
<?phpfunction smarty_version($args){ if ($GLOBALS[‘config‘][‘is_dev_site‘]){ $stat = stat($GLOBALS[‘config‘][‘site_root‘].$args[‘src‘]); $version = $stat[‘mtime‘]; }else{ $version = $GLOBALS[‘config‘][‘resource_versions‘][$args[‘src‘]]; } echo preg_replace(‘!.([a-z]+?)$!‘, ".v$version.$1", $args[‘src‘]);}?>
就這樣,不需要改文件名,也不需要記住改了哪些文件——當(dāng)文件有新版本發(fā)布時(shí)它的url就會(huì)自動(dòng)更新——有意思吧?我們就快搞定了。
之前談到為靜態(tài)文件發(fā)送超長周期(very-long-period)的緩存header時(shí)曾說過,如果不用php輸出,就不能輕易的發(fā)送緩存header。很顯然,有兩個(gè)辦法可以解決:用php輸出,或者讓apache來做。
php出馬,手到擒來。我們要做的僅僅是改變r(jià)ewrite規(guī)則,把靜態(tài)文件指向php腳本,用php在輸出文件內(nèi)容之前發(fā)送header。
Apache:RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /redir.php?path=$1$2 [L]PHP:header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");header("Cache-Control: max-age=315360000");# 忽略帶有“..”的路徑if (preg_match(‘!..!‘, $_GET[path])){ go_404(); }# 保證路徑開頭是確定的目錄if (!preg_match(‘!^(javascript|css|images)!‘, $_GET[path])){ go_404(); }# 文件不存在?if (!file_exists($_GET[path])){ go_404(); }# 發(fā)出一個(gè)文件類型header$ext = array_pop(explode(‘.‘, $_GET[path]));switch ($ext){ case ‘css‘: header("Content-type: text/css"); break; case ‘js‘ : header("Content-type: text/javascript"); break; case ‘gif‘: header("Content-type: image/gif"); break; case ‘jpg‘: header("Content-type: image/jpeg"); break; case ‘png‘: header("Content-type: image/png"); break; default: header("Content-type: text/plain");}# 輸出文件內(nèi)容echo implode(‘‘, file($_GET[path]));function go_404(){ header("HTTP/1.0 404 File not found"); exit;}
這個(gè)方案有效,但并不出色。(因?yàn)椋└鷄pache相比,php需要更多內(nèi)存和執(zhí)行時(shí)間。另外,我們還得小心防止可能由path參數(shù)傳遞偽造值引起的exploits。為避免這些問題,應(yīng)該用apache直接發(fā)送header。rewrite規(guī)則語句允許當(dāng)規(guī)則匹配時(shí)設(shè)置環(huán)境變量(environment variable),當(dāng)給定的環(huán)境變量設(shè)置后,Header命令就可以添加header。結(jié)合以下兩條語句,我們就把rewrite規(guī)則和header設(shè)置綁定在了一起:
RewriteEngine onRewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1]Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILEHeader add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE
考慮到apache的執(zhí)行順序,應(yīng)該把rewrite規(guī)則加在主配置文件(httpd.conf)而不是目錄配置文件(.htaccess)中。否則在環(huán)境變量設(shè)置之前,header行會(huì)先執(zhí)行(就那沒意義了)。至于header行,則可以放在兩文件任何一個(gè)當(dāng)中,沒什么區(qū)別。
(htmlor注:多謝tchaikov告知“skinning rabbits”的含義,但我不想翻的太正式,眼下的這個(gè)應(yīng)該不算太離譜吧。)
通過結(jié)合使用以上技術(shù),我們可以建立一個(gè)靈活的開發(fā)環(huán)境和一個(gè)快速又高性能的產(chǎn)品環(huán)境。當(dāng)然,這離終極目標(biāo)“速度”還有一段距離。有許多更深層的技術(shù)(比如分離伺服靜態(tài)內(nèi)容,用多域名提升并發(fā)量等)值得我們關(guān)注,包括與我們談到的方法(建立apache過濾器,修改資源url,加上版本信息)殊途同歸的其他路子。你可以留下評(píng)論,告訴我們那些你正在使用的卓有成效的技術(shù)和方法。
(完)
This entry was posted on 星期四, 八月 3rd, 2006 at 02:23:57 and is filed under javascript, web. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.
xiaoxiSays:
八月 3rd, 2006 at 12:06:44 e
現(xiàn)在電腦內(nèi)存很高,上網(wǎng)速度也快了很多,得像緩存、文件大小幾乎可以忽略不計(jì)吧
htmlorSays:
八月 3rd, 2006 at 12:22:17 e
呵呵,客戶端是可以忽略掉,但是服務(wù)器端呢?系統(tǒng)負(fù)載、資源開銷、流量等問題對(duì)大型網(wǎng)站是非常非常重要的。
大雄Says:
八月 3rd, 2006 at 17:41:54 e
不過好的程序,確實(shí)相差很多,不過最好所有的文件都是靜態(tài),這樣訪問的速度肯定快。
現(xiàn)在用ASP做,不知道IIS的性能如何。
my5151.meibu.comSays:
八月 3rd, 2006 at 17:43:49 e
收藏??! (可視化自定義web表單工具, 在:my5151.meibu.com )
zolaSays:
八月 4th, 2006 at 16:23:56 e
翻譯的不錯(cuò)!
SusanSays:
八月 4th, 2006 at 17:17:15 e
第一句話里面應(yīng)該是 下一代web …. 您打錯(cuò)了.
htmlorSays:
八月 4th, 2006 at 17:59:05 e
多謝susan,還真是手誤了。已改正。
modifySays:
八月 4th, 2006 at 18:28:22 e
to xiaoxi:
那是你家的電腦。。。。
我家的電腦就沒那么快。。。。
tchaikovSays:
八月 4th, 2006 at 20:48:41 e
“Skinning rabbits”應(yīng)該是英語中的一個(gè)俚語,相似的還有“there’s more than one way to skin a cat”。
和文中翻譯的一樣,都是“殊途同歸”的意思,不妨譯作“條條大路通羅馬”。
RieSays:
八月 4th, 2006 at 21:11:22 e
真是好文章,翻譯的也很符合中國人的閱讀思維
htmlorSays:
八月 4th, 2006 at 21:33:06 e
多謝tchaikov釋疑。又學(xué)了一招。
htmlorSays:
八月 4th, 2006 at 21:50:37 e
Rie
很高興你這么說。這篇文章能對(duì)看的人有所啟發(fā),我就最開心了。
xLightSays:
八月 5th, 2006 at 13:21:06 e
受教了,我要轉(zhuǎn)一個(gè)
om19Says:
八月 5th, 2006 at 19:09:33 e
雖然個(gè)人電腦快,網(wǎng)速看看網(wǎng)頁基本還好。但是緩存可以節(jié)省大量服務(wù)器資源~服務(wù)器資源是永遠(yuǎn)不夠的!
dengSays:
八月 5th, 2006 at 19:44:41 e
翻譯的不錯(cuò),雖然我不是搞js開發(fā)的,還是忍不住留個(gè)言(一般情況我直接就關(guān)窗口)
AmirFishSays:
八月 5th, 2006 at 22:29:05 e
感謝你的翻譯。受益匪淺。 :)
希望繼續(xù)翻譯更多的精品文章。。收藏了。
htmlorSays:
八月 5th, 2006 at 22:42:23 e
多謝大家捧場(chǎng)。以后看到有意思的文章,還是會(huì)翻的。做自己感興趣的事,動(dòng)力似乎特別大。
SikoSays:
八月 5th, 2006 at 23:32:57 e
翻譯的不錯(cuò)
http://computer.mblogger.cn/lugisSays:
八月 6th, 2006 at 19:04:26 e
收益!轉(zhuǎn)載下
xLightSays:
八月 8th, 2006 at 11:23:47 e
文章中沒有提到mod_headers這個(gè)apache模塊。
誰能告訴我哪里有下載mod_headers?
htmlorSays:
八月 8th, 2006 at 11:56:37 e
xLight
mod_headers無需下載,只要安裝apache時(shí)編譯進(jìn)去就行了(或者動(dòng)態(tài)加載也可以)。
toddSays:
八月 9th, 2006 at 21:14:07 e
好文。之前看過一次,不過不太全。
只有是制作大型應(yīng)用人,才能真正理解其中的意義。
Suave’s Blog ? Web Cache TutorialSays:
八月 10th, 2006 at 08:51:24 e
[…] Serving Javascript Fast (中文版) […]
聯(lián)系客服