在一個討論web技術(shù)的網(wǎng)站vitamin上發(fā)現(xiàn)這篇《Serving JavaScript Fast》,讀過之后大有收獲,茅塞頓開。于是就有了翻譯過來的念頭——我這人有個毛病,看到有意思的英文文章,就想自己翻過來(雖然英文水平很爛)。先在網(wǎng)上查了查,已經(jīng)有blog談到這篇文章(我算是后知后覺了),有總結(jié)要點的《Flickr 的開發(fā)者的 Web 應用優(yōu)化技巧》,也有延伸開來的《接著講Flickr的八卦》,但似乎沒有全文翻譯的(這下就好,不會忙了半天發(fā)現(xiàn)是無用功)。之后,就寫信問作者可不可以,作者一口答應:“sure - i’d love you to translate it”,只是要求我翻好之后給他一個鏈接地址。得到準許,心里就有底了。 先介紹一下作者。Cal Henderson,倫敦人,現(xiàn)居加利福尼亞的舊金山。PHP,MySQL和Perl專家,現(xiàn)任flickr架構(gòu)師(flickr被收購后就在yahoo了),同時也是vitamin的特聘顧問(寫些技術(shù)性文章)。 既然他是架構(gòu)師,flickr用的應該就是文中談到的這些技術(shù),于是參照文章,再對比網(wǎng)站,種種跡象表明確實如此。雖然在中國訪問flickr速度不敢恭維,加速效果不得而知,但其用了n多css和javascript資源卻似乎從沒出過什么問題,也從側(cè)面印證了這些技術(shù)的有效性。 仔細的看完文章,還有個強烈的感覺:這老兄也太能賣關(guān)子了,一句話非分成三句說,擺事實講道理是夠透徹,就是有點太@#$%了…… 算了,他怎么說我怎么翻吧,忠實于原著嘛,要不就成篡改了。經(jīng)過幾天努力,加上同事thincat兄傾力援手(小弟不勝感激?。K于完工(@_@ 真是苦力活啊,我再也不想干了~)。 全文翻譯如下: 讓javascript跑得更快作者:Cal Henderson 下一代web應用讓javascript和css得堪大用。我們會告訴你怎樣使這些應用又快又靈。 建立了號稱“Web 2.0”的應用,也實現(xiàn)了富內(nèi)容(rich content)和交互,我們期待著css和javascript扮演更加重要的角色。為使應用干凈利落,我們需要完善那些渲染頁面的文件,優(yōu)化其大小和形態(tài),以確保提供最好的用戶體驗——在實踐中,這就意味著一種結(jié)合:使內(nèi)容盡可能小、下載盡可能快,同時避免對未改動資源不必要的重新獲取。 由于css和js文件的形態(tài),情況有點復雜。跟圖片相比,其源代碼很有可能頻繁改動。而一旦改動,就需要客戶端重新下載,使本地緩存無效(保存在其他緩存里的版本也是如此)。在這篇文章里,我們將著重探討怎樣使用戶體驗最快:包括初始頁面的下載,隨后頁面的下載,以及隨著應用漸進、內(nèi)容變化而進行的資源下載。 我始終堅信這一點:對開發(fā)者來說,應該盡可能讓事情變得簡單。所以我們青睞于那些能讓系統(tǒng)自動處理優(yōu)化難題的方法。只需少許工作量,我們就能建立一舉多得的環(huán)境:它使開發(fā)變得簡單,有極佳的終端性能,也不會改變現(xiàn)有的工作方式。 好大一沱老的思路是,為優(yōu)化性能,可以把多個css和js文件合并成極少數(shù)大文件。跟十個5k的js文件相比,合并成一個50k的文件更好。雖然代碼總字節(jié)數(shù)沒變,卻避免了多個HTTP請求造成的開銷。每個請求都會在客戶端和服務器兩邊有個建立和消除的過程,導致請求和響應header帶來開銷,還有服務器端更多的進程和線程資源消耗(可能還有為壓縮內(nèi)容耗費的cpu時間)。 (除了HTTP請求,)并發(fā)問題也很重要。默認情況下,在使用持久連接(persistent connections)時,ie和firefox在同一域名內(nèi)只會同時下載兩個資源(在HTTP 1.1規(guī)格書中第8.1.4節(jié)的建議)(htmlor注:可以通過修改注冊表等方法改變這一默認配置)。這就意味著,在我們等待下載2個js文件的同時,將無法下載圖片資源。也就是說,這段時間內(nèi)用戶在頁面上看不到圖片。 (雖然合并文件能解決以上兩個問題,)可是,這個方法有兩個缺點。第一,把所有資源一起打包,將強制用戶一次下載完所有資源。如果(不這么做,而是)把大塊內(nèi)容變成多個文件,下載開銷就分散到了多個頁面,同時緩解了會話中的速度壓力(或完全避免了某些開銷,這取決于用戶選擇的路徑)。如果為了隨后頁面下載得更快而讓初始頁面下載得很慢,我們將發(fā)現(xiàn)更多用戶根本不會傻等著再去打開下一個頁面。 第二(這個影響更大,一直以來卻沒怎么被考慮過),在一個文件改動很頻繁的環(huán)境里,如果采用單文件系統(tǒng),那么每次改動文件都需要客戶端把所有css和js重新下載一遍。假如我們的應用有個100k的合成的js大文件,任何微小的改動都將強制客戶端把這100k再消化一遍。 分解之道(看來合并成大文件不太合適。)替代方案是個折中的辦法:把css和js資源分散成多個子文件,按功能劃分、保持文件個數(shù)盡可能少。這個方案也是有代價的,雖說開發(fā)時代碼分散成邏輯塊(logical chunks)能提高效率,可在下載時為提高性能還得合并文件。不過,只要給build系統(tǒng)(把開發(fā)代碼變成產(chǎn)品代碼的工具集,是為部署準備的)加點東西,就沒什么問題了。 對于有著不同開發(fā)和產(chǎn)品環(huán)境的應用來說,用些簡單的技術(shù)可以讓代碼更好管理。在開發(fā)環(huán)境下,為使條理清晰,代碼可以分散為多個邏輯部分(logical components)??梢栽?a href="http://smarty./">Smarty(一種php模板語言)里建立一個簡單的函數(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中會把“src”替換成不知所謂的字符,因此這里只有寫成“SOURCE”,使用代碼時請注意替換,下同) 就這么簡單。然后我們就命令build過程(build process)去把確定的文件合并起來。這個例子里,合并的是foo.js和bar.js,因為它們幾乎總是一起下載。我們能讓應用配置記住這一點,并修改模板函數(shù)去使用它。(代碼如下:) SMARTY: {insert_js files="foo.js,bar.js,baz.js"} PHP: # 源文件映射圖。在build過程合并文件之后用這個圖找到js的源文件。 $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> 模板里的源代碼沒必要為了分別適應開發(fā)和產(chǎn)品階段而改動,它幫助我們在開發(fā)時保持文件分散,發(fā)布成產(chǎn)品時把文件合并。想更進一步的話,可以把合并過程(merge process)寫在php里,然后使用同一個(合并文件的)配置去執(zhí)行。這樣就只有一個配置文件,避免了同步問題。為了做的更加完美,我們還可以分析css和js文件在頁面中同時出現(xiàn)的幾率,以此決定合并哪些文件最合理(幾乎總是同時出現(xiàn)的文件是合并的首選)。 對css來說,可以先建立一個主從關(guān)系的模型,它很有用。一個主樣式表控制應用的所有樣式表,多個子樣式表控制不同的應用區(qū)域。采用這個方法,大多數(shù)頁面只需下載兩個css文件,而其中一個(指主樣式表)在頁面第一次請求時就會緩存。 對沒有太多css和js資源的應用來說,這個方法在第一次請求時可能比單個大文件慢,但如果保持文件數(shù)量很少的話,你會發(fā)現(xiàn)其實它更快,因為每個頁面的數(shù)據(jù)量更小。讓人頭疼的下載花銷被分散到不同的應用區(qū)域,因此并發(fā)下載數(shù)保持在一個最小值,同時也使得頁面的平均下載數(shù)據(jù)量很小。 壓縮談到資源壓縮,大多數(shù)人馬上會想到mod_gzip(但要當心,mod_gzip實際上是個魔鬼,至少能讓人做惡夢)。它的原理很簡單:瀏覽器請求資源時,會發(fā)送一個header表明自己能接受的內(nèi)容編碼。就像這樣: Accept-Encoding: gzip,deflate 服務器遇到這樣的header請求時,就用gzip或deflate壓縮內(nèi)容發(fā)往客戶端,然后客戶端解壓縮。這過程減少了數(shù)據(jù)傳輸量,同時消耗了客戶端和服務器的cpu時間。也算差強人意。但是,mod_gzip的工作方式是這樣的:先在磁盤上創(chuàng)建一個臨時文件,然后發(fā)送(給客戶端),最后刪除這個文件。在高容量的系統(tǒng)中,由于磁盤io問題,很快就會達到極限。要避免這種情況,可以改用mod_deflate(apache 2才支持)。它采用更合理的方式:在內(nèi)存里做壓縮。對于apache 1的用戶來說,可以建立一塊ram磁盤,讓mod_gzip在它上面寫臨時文件。雖然沒有純內(nèi)存方式快,但也不會比往磁盤上寫文件慢。 話雖如此,其實還是有辦法完全避免壓縮開銷的,那就是預壓縮相關(guān)靜態(tài)資源,下載時由mod_gzip提供合適的壓縮版本。如果把壓縮添加在build過程,它就很透明了。需要壓縮的文件通常很少(用不著壓縮圖片,因為并不能減小更多體積),只有css和js文件(和其他未壓縮的靜態(tài)內(nèi)容)。 配置選項會告訴mod_gzip去哪里找到預壓縮過的文件。 mod_gzip_can_negotiate Yes mod_gzip_static_suffix .gz AddEncoding gzip .gz 新一點的mod_gzip版本(從1.3.26.1a開始)添加一個額外的配置選項后,就能自動預壓縮文件。不過在此之前,必須確認apache有正確的權(quán)限去創(chuàng)建和覆蓋壓縮文件。 mod_gzip_update_static Yes 可惜,事情沒那么簡單。某些Netscape 4的版本(尤其是4.06-4.08)認為自己能夠解釋壓縮內(nèi)容(它們發(fā)送一個header這么說來著),但其實它們不能正確的解壓縮。大多數(shù)其他版本的Netscape 4在下載壓縮內(nèi)容時也有各種各樣的問題。所以要在服務器端探測代理類型,(如果是Netscape 4,就要)讓它們得到未壓縮的版本。這還算簡單的。ie(版本4-6)有些更有意思的問題:當下載壓縮的javascript時,有時候ie會不正確的解壓縮文件,或者解壓縮到一半中斷,然后把這半個文件顯示在客戶端。如果你的應用對javascript的依賴比較大(htmlor注:比如ajax應用),那么就得避免發(fā)送壓縮文件給ie。在某些情況下,一些更老的5.x版本的ie倒是能正確的收到壓縮的javascript,可它們會忽略這個文件的etag header,不緩存它。(thincat友情提示:盡管壓縮存在一些瀏覽器不兼容的現(xiàn)象,由于這些不能很好的支持壓縮的瀏覽器數(shù)量現(xiàn)在已經(jīng)非常少了,我認為這種由于瀏覽器導致的壓縮不正常的情況可以忽略不計。這些過時的瀏覽器還能不能在現(xiàn)在流行的windows或unix環(huán)境下面安裝都存在不小的問題) 既然gzip壓縮有這么多問題,我們不妨把注意力轉(zhuǎn)到另一邊:不改變文件格式的壓縮?,F(xiàn)在有很多這樣的javascript壓縮腳本可用,大多數(shù)都用一個正則表達式驅(qū)動的語句集來減小源代碼的體積。它們做的不外乎幾件事:去掉注釋,壓縮空格,縮短私有變量名和去掉可省略的語法。 不幸的是,大多數(shù)腳本效果并不理想,要么壓縮率相當?shù)?,要么某種情形下會把代碼搞得一團糟(或者兩者兼而有之)。由于對解析樹的理解不完整,壓縮器很難區(qū)分一句注釋和一句看似注釋的引用字符串。因為閉合結(jié)構(gòu)的混合使用,要用正則表達式發(fā)現(xiàn)哪些變量是私有的并不容易,因此一些縮短變量名的技術(shù)會打亂某些閉合代碼。 還好有個壓縮器能避免這些問題:dojo壓縮器(現(xiàn)成的版本在這里)。它使用rhino(mozilla的javascript引擎,是用java實現(xiàn)的)建立一個解析樹,然后將其提交給文件。它能很好的減小代碼體積,僅用很小的成本:因為只在build時壓縮一次。由于壓縮是在build過程中實現(xiàn)的,所以一清二楚。(既然壓縮沒有問題了,)我們可以在源代碼里隨心所欲的添加空格和注釋,而不必擔心影響到產(chǎn)品代碼。 與javascript相比,css文件的壓縮相對簡單一些。由于css語法里不會有太多引用字符串(通常是url路徑跟字體名),我們可以用正則表達式大刀闊斧的干掉空格(htmlor注:這句翻的最爽,哈哈)。如果確實有引用字符串的話,我們總可以把一串空格合成一個(因為不需要在url路徑和字體名里查找多個空格和tab)。這樣的話,一個簡單的perl腳本就夠了: #!/usr/bin/perl my $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é)束大括號后添加換行 $data =~ s!\n$!!; # 刪除最后一個換行 $data =~ s! { ! {!g; # 去除開始大括號后的空格 $data =~ s!; }!}!g; # 去除結(jié)束大括號前的空格 print $data; 然后,就可以把單個的css文件傳給腳本去壓縮了。命令如下: perl compress.pl site.source.css > site.compress.css 做完這些簡單的純文本優(yōu)化工作后,我們就能減少數(shù)據(jù)傳輸量多達50%了(這個量取決于你的代碼格式,可能更多)。這帶來了更快的用戶體驗。不過我們真正想做的是,盡可能避免用戶請求的發(fā)生——除非確實有必要。這下HTTP緩存知識派上用場了。 緩存是好東西當用戶代理(如瀏覽器)向服務器請求一個資源時,第一次請求過后它就會緩存服務器的響應,以避免重復之后的相同請求。緩存時間的長短取決于兩個因素:代理的配置和服務器的緩存控制header。所有瀏覽器都有不同的配置選項和處理方式,但大多數(shù)都會把一個資源至少緩存到會話結(jié)束(除非被明確告知)。 為了不讓瀏覽器緩存改動頻繁的頁面,你很可能已經(jīng)發(fā)送過header不緩存動態(tài)內(nèi)容。在php中,以下兩行命令可以做到: <?php header("Cache-Control: private"); header("Cache-Control: no-cache", false); ?> 聽起來太簡單了?確實如此——因為有些代理(瀏覽器)在某些環(huán)境下將忽略這些header。要確保瀏覽器不緩存文檔,應該更強硬一些: <?php # 讓它在過去就“失效” header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); # 永遠是改動過的 header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); # HTTP/1.1 header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); # HTTP/1.0 header("Pragma: no-cache"); ?> 這樣,對于我們不想緩存的內(nèi)容來說已經(jīng)行了。但對于那些不會每次請求時都有改動的內(nèi)容,應該鼓勵瀏覽器更霸道的緩存它?!癐f-Modified-Since”請求header能夠做到這點。如果客戶端在請求中發(fā)送一個“If-Modified-Since”header,apache(或其他服務器)會以狀態(tài)代碼304(沒改過)響應,告訴瀏覽器緩存已經(jīng)是最新的。使用這個機制,能夠避免重復發(fā)送文件給瀏覽器,不過仍然導致了一個HTTP請求的消耗。嗯,再想想。 與If-Modified-Since機制類似的是實體標記(entity tags)。在apache環(huán)境下,每個對靜態(tài)文件的響應都會發(fā)出一個“ETag”header,它包含了一個由文件修改時間、文件大小和inode號生成的校驗和(checksum)。在下載文件之前,瀏覽器會發(fā)送一個HEAD請求去檢查文件的etag。可ETag跟If-Modified-Since有同樣的問題:客戶端仍舊需要執(zhí)行HTTP請求來驗證本地緩存是否有效。 此外,如果你使用多臺服務器提供內(nèi)容,得小心使用if-modified-since和etags。在兩臺負載平衡的服務器環(huán)境下,對一個代理(瀏覽器)來說,一個資源可以這次從A服務器得到,下次從B服務器得到(htmlor注:lvs負載平衡系統(tǒng)就是個典型的例子)。這很好,也是采用平衡負載的原因??墒?,如果兩臺服務器給同一個文件生成了不同的etag或者文件修改日期,瀏覽器就無所適從了(每次都會重新下載)。默認情況下,etag是由文件的inode號生成的,而多臺服務器之間文件的inode號是不同的。可以使用apache的配置選項關(guān)掉它: FileETag MTime Size 使用這個選項,apache將只用文件修改日期和文件大小來決定etag。很不幸,這導致了另一個問題(一樣能影響if-modified-since)。既然etag依賴于修改時間,就得讓時間同步??赏嗯_服務器上傳文件時,上傳時間差個一到兩秒是常有的事。這樣一來,兩臺服務器生成的etag還是不一樣。當然,我們還可以改變配置,讓etag的生成只取決于文件大小,但這就意味著如果文件內(nèi)容變了而大小沒變,etag也不會變。這可不行。 緩存真是個好東西看來我們正從錯誤的方向入手解決問題。(現(xiàn)在的問題是,)這些可能的緩存策略導致了一件事情反復發(fā)生,那就是:客戶端向服務器查詢本地緩存是否最新。假如服務器在改動文件的時候通知客戶端,客戶端不就知道它的緩存是最新的了(直到接到下一次通知)?可惜天公不做美——(事實)是客戶端向服務器發(fā)出請求。 其實,也不盡然。在獲取js或css文件之前,客戶端會用<script>或<link>標記向服務器發(fā)送一個請求,說明哪個頁面要加載這些文件。這時候就可以用服務器的響應來通知客戶端這些文件有了改動。有點含糊,說得再詳細點就是:如果改變css和js文件內(nèi)容的同時,也改變它們的文件名,就可以告訴客戶端對url全都永久緩存——因為每個url都是唯一的。 假如能確定一個資源永不更改,我們就可以發(fā)出一些霸氣十足的緩存header(htmlor注:這句也很有氣勢吧)。在php里,兩行就好: <?php header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT"); header("Cache-Control: max-age=315360000"); ?> 我們告訴瀏覽器這個內(nèi)容在10年后(10年大概會有315,360,000秒,或多或少)過期,瀏覽器將會保留它10年。當然,很有可能不用php輸出css和js文件(因此就不能發(fā)出header),這種情況將在稍后說明。 人力有時而窮當文件內(nèi)容更改時,手動去改文件名是很危險的。假如你改了文件名,模板卻沒有指向它?假如你改了一些模板另一些卻沒改?假如你改了模板卻沒改文件名?還有最糟的,假如你改動了文件卻忘了改名或者忘了改變對它的引用?最好的結(jié)果,是用戶看到老的而看不到新的內(nèi)容。最壞的結(jié)果,是找不到文件,網(wǎng)站沒法運轉(zhuǎn)了。聽起來這(指改動文件內(nèi)容時修改url)似乎是個餿主意。 幸運的是,計算機做這類事情——當某種變化發(fā)生,需要相當準確地完成的、重復重復再重復的(htmlor注:番茄雞蛋伺候~)、枯燥乏味的工作——總是十分在行。 這個過程(改變文件的url)沒那么痛苦,因為我們根本不需要改文件名。資源的url和磁盤上文件的位置也沒必要保持一致。使用apache的mod_rewrite模塊,可以建立簡單的規(guī)則,讓確定的url重定向到確定的文件。 RewriteEngine on RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L] 這條規(guī)則匹配任何帶有指定擴展名同時含有“版本”信息(version nugget)的url,它會把這些url重定向到一個不含版本信息的路徑。如下所示: 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(因為版本號變了)。由于url變了,瀏覽器就認為它是另一個資源(會重新下載)。想更進一步的話,可以把我們之前說的腳本編組函數(shù)結(jié)合起來,根據(jù)需要生成一個帶有版本號的<script>標記列表。 說到這里,你可能會問我,為什么不在url結(jié)尾加一個查詢字符串(query string)呢(如/css/main.css?v=4)?根據(jù)HTTP緩存規(guī)格書所說,用戶代理對含有查詢字符串的url永不緩存。雖然ie跟firefox忽略了這點,opera和safari卻沒有——為了確保所有瀏覽器都緩存你的資源,還是不要在url里用查詢字符串的好。 現(xiàn)在不移動文件就能更改url了,如果能讓url自動更新就更好了。在小型的產(chǎn)品環(huán)境下(如果有大型的產(chǎn)品環(huán)境,就是開發(fā)環(huán)境了),使用模板功能可以很輕易的實現(xià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" /> 對每個鏈接到的資源文件,我們得到它在磁盤上的路徑,檢查它的mtime(文件最后修改的日期和時間),然后把這個時間當作版本號插入到url中。對于低流量的站點(它們的stat操作開銷不大)或者開發(fā)環(huán)境來說,這個方案不錯,但對于高容量的環(huán)境就不適用了——因為每次stat操作都要磁盤讀?。▽е路掌髫撦d升高)。 解決方案相當簡單。在大型系統(tǒng)中每個資源都已經(jīng)有了一個版本號,就是版本控制的修訂號(你們應該使用了版本控制,對吧?)。當我們建立站點準備部署的時候,可以輕易的查到每個文件的修訂號,寫在一個靜態(tài)配置文件里。 <?php $GLOBALS[‘config‘][‘resource_versions‘] = array( ‘/images/foo.gif‘ => ‘2.1‘, ‘/css/main.css‘ => ‘1.27‘, ‘/javascript/md5.js‘ => ‘6.1.4‘, ); ?> 當我們發(fā)布產(chǎn)品時,可以修改模板函數(shù)來使用版本號。 <?php function 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‘]); } ?> 就這樣,不需要改文件名,也不需要記住改了哪些文件——當文件有新版本發(fā)布時它的url就會自動更新——有意思吧?我們就快搞定了。 只欠東風之前談到為靜態(tài)文件發(fā)送超長周期(very-long-period)的緩存header時曾說過,如果不用php輸出,就不能輕易的發(fā)送緩存header。很顯然,有兩個辦法可以解決:用php輸出,或者讓apache來做。 php出馬,手到擒來。我們要做的僅僅是改變rewrite規(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ā)出一個文件類型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; } 這個方案有效,但并不出色。(因為)跟apache相比,php需要更多內(nèi)存和執(zhí)行時間。另外,我們還得小心防止可能由path參數(shù)傳遞偽造值引起的exploits。為避免這些問題,應該用apache直接發(fā)送header。rewrite規(guī)則語句允許當規(guī)則匹配時設置環(huán)境變量(environment variable),當給定的環(huán)境變量設置后,Header命令就可以添加header。結(jié)合以下兩條語句,我們就把rewrite規(guī)則和header設置綁定在了一起: RewriteEngine on RewriteRule ^/(.*.)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_FILE Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE 考慮到apache的執(zhí)行順序,應該把rewrite規(guī)則加在主配置文件(httpd.conf)而不是目錄配置文件(.htaccess)中。否則在環(huán)境變量設置之前,header行會先執(zhí)行(就那沒意義了)。至于header行,則可以放在兩文件任何一個當中,沒什么區(qū)別。 眼觀六路(htmlor注:多謝tchaikov告知“skinning rabbits”的含義,但我不想翻的太正式,眼下的這個應該不算太離譜吧。) 通過結(jié)合使用以上技術(shù),我們可以建立一個靈活的開發(fā)環(huán)境和一個快速又高性能的產(chǎn)品環(huán)境。當然,這離終極目標“速度”還有一段距離。有許多更深層的技術(shù)(比如分離伺服靜態(tài)內(nèi)容,用多域名提升并發(fā)量等)值得我們關(guān)注,包括與我們談到的方法(建立apache過濾器,修改資源url,加上版本信息)殊途同歸的其他路子。你可以留下評論,告訴我們那些你正在使用的卓有成效的技術(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. 23 Responses to “flickr對javascript干的好事”
|
|
來自: 達能牛牛 > 《JavaScript》