讓javascript跑得更快

來源:互聯網
上載者:User

在一個討論web技術的網站vitamin上發現這篇《Serving JavaScript Fast》,讀過之後大有收穫,茅塞頓開。於是就有了翻譯過來的念頭——我這人有個毛病,看到有意思的英文文章,就想自己翻過來(雖然英文水平很爛)。先在網上查了查,已經有blog談到這篇文章(我算是後知後覺了),有總結要點的《Flickr 的開發人員的 Web 應用程式最佳化技巧》,也有延伸開來的《接著講Flickr的八卦》,但似乎沒有全文翻譯的(這下就好,不會忙了半天發現是無用功)。之後,就寫信問作者可不可以,作者一口答應:“sure - i’d love you to translate it”,只是要求我翻好之後給他一個連結地址。得到准許,心裡就有底了。 先介紹一下作者。Cal Henderson,倫敦人,現居加利福尼亞的舊金山。PHP,MySQL和Perl專家,現任flickr架構師(flickr被收購後就在yahoo了),同時也是vitamin的特聘顧問(寫些技術性文章)。

既然他是架構師,flickr用的應該就是文中談到的這些技術,於是參照文章,再對比網站,種種跡象表明確實如此。雖然在中國訪問flickr速度不敢恭維,加速效果不得而知,但其用了n多css和javascript資源卻似乎從沒出過什麼問題,也從側面印證了這些技術的有效性。

仔細的看完文章,還有個強烈的感覺:這老兄也太能賣關子了,一句話非分成三句說,擺事實講道理是夠透徹,就是有點太@#$%了…… 算了,他怎麼說我怎麼翻吧,忠實於原著嘛,要不就成篡改了。經過幾天努力,加上同事thincat兄傾力援手(小弟不勝感激啊),終於完工(@_@ 真是苦力活啊,我再也不想幹了~)。

全文翻譯如下:

讓javascript跑得更快

作者:Cal Henderson

下一代web應用讓javascript和css得堪大用。我們會告訴你怎樣使這些應用又快又靈。

建立了號稱“Web 2.0”的應用,也實現了富內容(rich content)和互動,我們期待著css和javascript扮演更加重要的角色。為使應用乾淨利落,我們需要完善那些渲染頁面的檔案,最佳化其大小和形態,以確保提供最好的使用者體驗——在實踐中,這就意味著一種結合:使內容儘可能小、下載儘可能快,同時避免對未改動資源不必要的重新擷取。

由於css和js檔案的形態,情況有點複雜。跟圖片相比,其原始碼很有可能頻繁改動。而一旦改動,就需要用戶端重新下載,使本機快取無效(儲存在其他緩衝裡的版本也是如此)。在這篇文章裡,我們將著重探討怎樣使使用者體驗最快:包括初始頁面的下載,隨後頁面的下載,以及隨著應用漸進、內容變化而進行的資源下載。

我始終堅信這一點:對開發人員來說,應該儘可能讓事情變得簡單。所以我們青睞於那些能讓系統自動處理最佳化難題的方法。只需少許工作量,我們就能建立一舉多得的環境:它使開發變得簡單,有極佳的終端效能,也不會改變現有的工作方式。

好大一沱

老的思路是,為最佳化效能,可以把多個css和js檔案合并成極少數大檔案。跟十個5k的js檔案相比,合并成一個50k的檔案更好。雖然代碼總位元組數沒變,卻避免了多個HTTP請求造成的開銷。每個請求都會在用戶端和伺服器兩邊有個建立和消除的過程,導致請求和響應header帶來開銷,還有伺服器端更多的進程和線程資源消耗(可能還有為壓縮內容耗費的cpu時間)。

(除了HTTP請求,)並發問題也很重要。預設情況下,在使用持久串連(persistent connections)時,ie和firefox在同一網域名稱內只會同時下載兩個資源(在HTTP 1.1規格書中第8.1.4節的建議)(htmlor註:可以通過修改註冊表等方法改變這一預設配置)。這就意味著,在我們等待下載2個js檔案的同時,將無法下載圖片資源。也就是說,這段時間內使用者在頁面上看不到圖片。

(雖然合并檔案能解決以上兩個問題,)可是,這個方法有兩個缺點。第一,把所有資源一起打包,將強制使用者一次下載完所有資源。如果(不這麼做,而是)把大塊內容變成多個檔案,下載開銷就分散到了多個頁面,同時緩解了會話中的速度壓力(或完全避免了某些開銷,這取決於使用者選擇的路徑)。如果為了隨後頁面下載得更快而讓初始頁面下載得很慢,我們將發現更多使用者根本不會傻等著再去開啟下一個頁面。

第二(這個影響更大,一直以來卻沒怎麼被考慮過),在一個檔案改動很頻繁的環境裡,如果採用單檔案系統,那麼每次改動檔案都需要用戶端把所有css和js重新下載一遍。假如我們的應用有個100k的合成的js大檔案,任何微小的改動都將強制用戶端把這100k再消化一遍。

分解之道

(看來合并成大檔案不太合適。)替代方案是個折中的辦法:把css和js資源分散成多個子檔案,按功能劃分、保持檔案個數儘可能少。這個方案也是有代價的,雖說開發時代碼分散成邏輯塊(logical chunks)能提高效率,可在下載時為提高效能還得合并檔案。不過,只要給build系統(把開發代碼變成產品代碼的工具集,是為部署準備的)加點東西,就沒什麼問題了。

對於有著不同開發和產品環境的應用來說,用些簡單的技術可以讓代碼更好管理。在開發環境下,為使條理清晰,代碼可以分散為多個邏輯部分(logical components)。可以在Smarty(一種php範本語言)裡建立一個簡單的函數來管理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,因為它們幾乎總是一起下載。我們能讓應用配置記住這一點,並修改模板函數去使用它。(代碼如下:)

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>

模板裡的原始碼沒必要為了分別適應開發和產品階段而改動,它協助我們在開發時保持檔案分散,發布成產品時把檔案合并。想更進一步的話,可以把合并過程(merge process)寫在php裡,然後使用同一個(合并檔案的)配置去執行。這樣就只有一個設定檔,避免了同步問題。為了做的更加完美,我們還可以分析css和js檔案在頁面中同時出現的幾率,以此決定合并哪些檔案最合理(幾乎總是同時出現的檔案是合并的首選)。

對css來說,可以先建立一個主從關係的模型,它很有用。一個主樣式表控制應用的所有樣式表,多個子樣式表控制不同的應用地區。採用這個方法,大多數頁面只需下載兩個css檔案,而其中一個(指主樣式表)在頁面第一次請求時就會緩衝。

對沒有太多css和js資源的應用來說,這個方法在第一次請求時可能比單個大檔案慢,但如果保持檔案數量很少的話,你會發現其實它更快,因為每個頁面的資料量更小。讓人頭疼的下載花銷被分散到不同的應用地區,因此並發下載數保持在一個最小值,同時也使得頁面的平均下載資料量很小。

壓縮

談到資源壓縮,大多數人馬上會想到mod_gzip(但要當心,mod_gzip實際上是個魔鬼,至少能讓人做惡夢)。它的原理很簡單:瀏覽器請求資源時,會發送一個header表明自己能接受的內容編碼。就像這樣:

Accept-Encoding: gzip,deflate

伺服器遇到這樣的header請求時,就用gzip或deflate壓縮內容發往用戶端,然後用戶端解壓縮。這過程減少了資料轉送量,同時消耗了用戶端和伺服器的cpu時間。也算差強人意。但是,mod_gzip的工作方式是這樣的:先在磁碟上建立一個臨時檔案,然後發送(給用戶端),最後刪除這個檔案。在高容量的系統中,由於磁碟io問題,很快就會達到極限。要避免這種情況,可以改用mod_deflate(apache 2才支援)。它採用更合理的方式:在記憶體裡做壓縮。對於apache 1的使用者來說,可以建立一塊ram磁碟,讓mod_gzip在它上面寫臨時檔案。雖然沒有純記憶體方式快,但也不會比往磁碟上寫檔案慢。

話雖如此,其實還是有辦法完全避免壓縮開銷的,那就是預壓縮相關靜態資源,下載時由mod_gzip提供合適的壓縮版本。如果把壓縮添加在build過程,它就很透明了。需要壓縮的檔案通常很少(用不著壓縮圖片,因為並不能減小更多體積),只有css和js檔案(和其他未壓縮的靜態內容)。

配置選項會告訴mod_gzip去哪裡找到預壓縮過的檔案。

mod_gzip_can_negotiate    Yes
mod_gzip_static_suffix    .gz
AddEncoding    gzip    .gz

新一點的mod_gzip版本(從1.3.26.1a開始)添加一個額外的配置選項後,就能自動預壓縮檔。不過在此之前,必須確認apache有正確的許可權去建立和覆蓋壓縮檔。

mod_gzip_update_static    Yes

可惜,事情沒那麼簡單。某些Netscape 4的版本(尤其是4.06-4.08)認為自己能夠解釋壓縮內容(它們發送一個header這麼說來著),但其實它們不能正確的解壓縮。大多數其他版本的Netscape 4在下載壓縮內容時也有各種各樣的問題。所以要在伺服器端探測代理類型,(如果是Netscape 4,就要)讓它們得到未壓縮的版本。這還算簡單的。ie(版本4-6)有些更有意思的問題:當下載壓縮的javascript時,有時候ie會不正確的解壓縮檔案,或者解壓縮到一半中斷,然後把這半個檔案顯示在用戶端。如果你的應用對javascript的依賴比較大(htmlor註:比如ajax應用),那麼就得避免發送壓縮檔給ie。在某些情況下,一些更老的5.x版本的ie倒是能正確的收到壓縮的javascript,可它們會忽略這個檔案的etag header,不緩衝它。(thincat友情提示:儘管壓縮存在一些瀏覽器不相容的現象,由於這些不能很好的支援壓縮的瀏覽器數量現在已經非常少了,我認為這種由於瀏覽器導致的壓縮不正常的情況可以忽略不計。這些過時的瀏覽器還能不能在現在流行的windows或unix環境下面安裝都存在不小的問題)

既然gzip壓縮有這麼多問題,我們不妨把注意力轉到另一邊:不改變檔案格式的壓縮。現在有很多這樣的javascript壓縮指令碼可用,大多數都用一個Regex驅動的語句集來減小原始碼的體積。它們做的不外乎幾件事:去掉注釋,壓縮空格,縮短私人變數名和去掉可省略的文法。

不幸的是,大多數指令碼效果並不理想,要麼壓縮率相當低,要麼某種情形下會把代碼搞得一團糟(或者兩者兼而有之)。由於對解析樹的理解不完整,壓縮器很難區分一句注釋和一句看似注釋的引用字串。因為閉合結構的混合使用,要用Regex發現哪些變數是私人的並不容易,因此一些縮短變數名的技術會打亂某些閉合代碼。

還好有個壓縮器能避免這些問題:dojo壓縮器(現成的版本在這裡)。它使用rhino(mozilla的javascript引擎,是用java實現的)建立一個解析樹,然後將其提交給檔案。它能很好的減小代碼體積,僅用很小的成本:因為只在build時壓縮一次。由於壓縮是在build過程中實現的,所以一清二楚。(既然壓縮沒有問題了,)我們可以在原始碼裡隨心所欲的添加空格和注釋,而不必擔心影響到產品代碼。

與javascript相比,css檔案的壓縮相對簡單一些。由於css文法裡不會有太多引用字串(通常是url路徑跟字型名),我們可以用Regex大刀闊斧的幹掉空格(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;         # 在結束大括弧後添加換行
$data =~ s!/n$!!;             # 刪除最後一個換行
$data =~ s! { ! {!g;         # 去除開始大括弧後的空格
$data =~ s!; }!}!g;          # 去除結束大括弧前的空格
print $data;

然後,就可以把單個的css檔案傳給指令碼去壓縮了。命令如下:

perl compress.pl site.source.css > site.compress.css

做完這些簡單的純文字最佳化工作後,我們就能減少資料轉送量多達50%了(這個量取決於你的代碼格式,可能更多)。這帶來了更快的使用者體驗。不過我們真正想做的是,儘可能避免使用者請求的發生——除非確實有必要。這下HTTP緩衝知識派上用場了。

緩衝是好東西

當使用者代理程式(如瀏覽器)向伺服器請求一個資源時,第一次請求過後它就會快取服務器的響應,以避免重複之後的相同請求。緩衝時間的長短取決於兩個因素:代理的配置和伺服器的緩衝控制header。所有瀏覽器都有不同的配置選項和處理方式,但大多數都會把一個資源至少緩衝到會話結束(除非被明確告知)。

為了不讓瀏覽器緩衝改動頻繁的頁面,你很可能已經發送過header不緩衝動態內容。在php中,以下兩行命令可以做到:

<?php
header("Cache-Control: private");
header("Cache-Control: no-cache", false);
?>

聽起來太簡單了?確實如此——因為有些代理(瀏覽器)在某些環境下將忽略這些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");
?>

這樣,對於我們不想緩衝的內容來說已經行了。但對於那些不會每次請求時都有改動的內容,應該鼓勵瀏覽器更霸道的緩衝它。“If-Modified-Since”請求header能夠做到這點。如果用戶端在請求中發送一個“If-Modified-Since”header,apache(或其他伺服器)會以狀態碼304(沒改過)響應,告訴瀏覽器緩衝已經是最新的。使用這個機制,能夠避免重複傳送檔案給瀏覽器,不過仍然導致了一個HTTP請求的消耗。嗯,再想想。

與If-Modified-Since機制類似的是實體標記(entity tags)。在apache環境下,每個對靜態檔案的響應都會發出一個“ETag”header,它包含了一個由檔案修改時間、檔案大小和inode號產生的校正和(checksum)。在下載檔案之前,瀏覽器會發送一個HEAD請求去檢查檔案的etag。可ETag跟If-Modified-Since有同樣的問題:用戶端仍舊需要執行HTTP請求來驗證本機快取是否有效。

此外,如果你使用多台伺服器提供內容,得小心使用if-modified-since和etags。在兩台Server Load Balancer的伺服器環境下,對一個代理(瀏覽器)來說,一個資源可以這次從A伺服器得到,下次從B伺服器得到(htmlor註:lvsServer Load Balancer系統就是個典型的例子)。這很好,也是採用平衡負載的原因。可是,如果兩台伺服器給同一個檔案產生了不同的etag或者檔案修改日期,瀏覽器就無所適從了(每次都會重新下載)。預設情況下,etag是由檔案的inode號產生的,而多台伺服器之間檔案的inode號是不同的。可以使用apache的配置選項關掉它:

FileETag MTime Size

使用這個選項,apache將只用檔案修改日期和檔案大小來決定etag。很不幸,這導致了另一個問題(一樣能影響if-modified-since)。既然etag依賴於修改時間,就得讓時間同步。可往多台伺服器上傳檔案時,上傳時間差個一到兩秒是常有的事。這樣一來,兩台伺服器產生的etag還是不一樣。當然,我們還可以改變更配置置,讓etag的產生只取決於檔案大小,但這就意味著如果檔案內容變了而大小沒變,etag也不會變。這可不行。

緩衝真是個好東西

看來我們正從錯誤的方向入手解決問題。(現在的問題是,)這些可能的緩衝策略導致了一件事情反覆發生,那就是:用戶端向伺服器查詢本機快取是否最新。假如伺服器在改動檔案的時候通知用戶端,用戶端不就知道它的緩衝是最新的了(直到接到下一次通知)?可惜天公不做美——(事實)是用戶端向伺服器發出請求。

其實,也不盡然。在擷取js或css檔案之前,用戶端會用<script>或<link>標記向伺服器發送一個請求,說明哪個頁面要載入這些檔案。這時候就可以用伺服器的響應來通知用戶端這些檔案有了改動。有點含糊,說得再詳細點就是:如果改變css和js檔案內容的同時,也改變它們的檔案名稱,就可以告訴用戶端對url全都永久緩衝——因為每個url都是唯一的。

假如能確定一個資源永不更改,我們就可以發出一些霸氣十足的緩衝header(htmlor註:這句也很有氣勢吧)。在php裡,兩行就好:

<?php
header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");
header("Cache-Control: max-age=315360000");
?>

我們告訴瀏覽器這個內容在10年後(10年大概會有315,360,000秒,或多或少)到期,瀏覽器將會保留它10年。當然,很有可能不用php輸出css和js檔案(因此就不能發出header),這種情況將在稍後說明。

人力有時而窮

當檔案內容更改時,手動去改檔案名稱是很危險的。假如你改了檔案名稱,模板卻沒有指向它?假如你改了一些模板另一些卻沒改?假如你改了模板卻沒改檔案名稱?還有最糟的,假如你改動了檔案卻忘了改名或者忘了改變對它的引用?最好的結果,是使用者看到老的而看不到新的內容。最壞的結果,是找不到檔案,網站沒法運轉了。聽起來這(指改動檔案內容時修改url)似乎是個餿主意。

幸運的是,電腦做這類事情——當某種變化發生,需要相當準確地完成的、重複重複再重複的(htmlor註:番茄雞蛋伺候~)、枯燥乏味的工作——總是十分在行。

這個過程(改變檔案的url)沒那麼痛苦,因為我們根本不需要改檔案名稱。資源的url和磁碟上檔案的位置也沒必要保持一致。使用apache的mod_rewrite模組,可以建立簡單的規則,讓確定的url重新導向到確定的檔案。

RewriteEngine on
RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$    /$1$2    [L]

這條規則匹配任何帶有指定副檔名同時含有“版本”資訊(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

使用這條規則,就可以做到不改變檔案路徑而更改url(因為版本號碼變了)。由於url變了,瀏覽器就認為它是另一個資源(會重新下載)。想更進一步的話,可以把我們之前說的指令碼編組函數結合起來,根據需要產生一個帶有版本號碼的<script>標記列表。

說到這裡,你可能會問我,為什麼不在url結尾加一個查詢字串(query string)呢(如/css/main.css?v=4)?根據HTTP緩衝規格書所說,使用者代理程式對含有查詢字串的url永不緩衝。雖然ie跟firefox忽略了這點,opera和safari卻沒有——為了確保所有瀏覽器都緩衝你的資源,還是不要在url裡用查詢字串的好。

現在不移動檔案就能更改url了,如果能讓url自動更新就更好了。在小型的產品環境下(如果有大型的產品環境,就是開發環境了),使用模板功能可以很輕易的實現這點。這裡用的是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操作開銷不大)或者開發環境來說,這個方案不錯,但對於高容量的環境就不適用了——因為每次stat操作都要磁碟讀取(導致伺服器負載升高)。

解決方案相當簡單。在大型系統中每個資源都已經有了一個版本號碼,就是版本控制的修訂編號(你們應該使用了版本控制,對吧?)。當我們建立網站準備部署的時候,可以輕易的查到每個檔案的修訂編號,寫在一個靜態設定檔裡。

<?php
$GLOBALS['config']['resource_versions'] = array(
  '/images/foo.gif'    => '2.1',
  '/css/main.css'      => '1.27',
  '/javascript/md5.js' => '6.1.4',
);
?>

當我們發布產品時,可以修改模板函數來使用版本號碼。

<?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']);
}
?>

就這樣,不需要改檔案名稱,也不需要記住改了哪些檔案——當檔案有新版本發布時它的url就會自動更新——有意思吧?我們就快搞定了。

只欠東風

之前談到為靜態檔案發送超長周期(very-long-period)的緩衝header時曾說過,如果不用php輸出,就不能輕易的發送緩衝header。很顯然,有兩個辦法可以解決:用php輸出,或者讓apache來做。

php出馬,手到擒來。我們要做的僅僅是改變rewrite規則,把靜態檔案指向php指令碼,用php在輸出檔案內容之前發送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(); }
# 發出一個檔案類型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");
}
# 輸出檔案內容
echo implode('', file($_GET[path]));
function go_404(){
  header("HTTP/1.0 404 File not found");
  exit;
}

這個方案有效,但並不出色。(因為)跟apache相比,php需要更多記憶體和執行時間。另外,我們還得小心防止可能由path參數傳遞偽造值引起的exploits。為避免這些問題,應該用apache直接發送header。rewrite規則語句允許當規則匹配時設定環境變數(environment variable),當給定的環境變數設定後,Header命令就可以添加header。結合以下兩條語句,我們就把rewrite規則和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的執行順序,應該把rewrite規則加在主設定檔(httpd.conf)而不是目錄設定檔(.htaccess)中。否則在環境變數設定之前,header行會先執行(就那沒意義了)。至於header行,則可以放在兩檔案任何一個當中,沒什麼區別。

眼觀六路

(htmlor註:多謝tchaikov告知“skinning rabbits”的含義,但我不想翻的太正式,眼下的這個應該不算太離譜吧。)

通過結合使用以上技術,我們可以建立一個靈活的開發環境和一個快速又高效能的產品環境。當然,這離終極目標“速度”還有一段距離。有許多更深層的技術(比如分離伺服靜態內容,用多網域名稱提升並發量等)值得我們關注,包括與我們談到的方法(建立apache過濾器,修改資源url,加上版本資訊)殊途同歸的其他路子。你可以留下評論,告訴我們那些你正在使用的卓有成效的技術和方法。

(完)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.