HTTP—快取

白牙青森發表於2014-12-16

 

1. ETag

HTTP 1.1中引入了ETag來解決快取的問題。ETag全稱是Entity Tag,由服務端生成,服務端可以決定它的生成規則。如果根據檔案內容生成雜湊值。那麼條件請求將不會受到時間戳的改動造成頻寬浪費。下面是根據內容生成雜湊值的方法:

1 var getHash = function(str) {
2   var shasum = crypto.createHash('sha1');
3   return shasum.update(str).digest('base64');    
4 }

與If-Modified-Since/Last-Modified不同的是,ETag的請求和響應是If-None-Match/ETag。瀏覽器在收到帶有ETag:'14-389247298365'欄位的響應頭後,會在後面的請求中將其設定在請求頭中:If-None-Match: '14-389247298365'。伺服器端收到帶If-None-Match: '14-389247298365'的報頭後,會進行如下判斷來決定返回新的內容還是隻響應一個304狀態碼讓瀏覽器使用本地快取版本:

 1 var handle = function(req, res) {
 2     fs.readFile(filename, function(err, file){
 3         var hash = getHash(file);
 4         var noneMatch = req['if-none-match'];
 5         if (hash === noneMatch) {
 6             res.writeHead(304, "Not Modified");
 7             res.end();
 8         } else {
 9             res.setHeader("ETag", hash);
10             res.writeHead(200, "OK");
11             res.end(file);
12         }
13     })
14 }

2. Last-Modified

通常來說,如果請求頭中不包含ETag,服務端會通過判斷Last-Modified值來決定響應304狀態碼還是新的檔案內容。Last-Modified顧名思義指的是檔案的最後一次修改時間。與ETag一樣,在瀏覽器首次訪問站點後,服務端會在其響應頭中設定一個Last-Modified的欄位,它的值是一個UTC格式的時間字串。隨後,在瀏覽器對站點的第二次訪問中,會在其請求頭中設定一個If-Modified-Since,其值就是上一次返回的Last-Modified的值。伺服器端會根據這個值是否與其本地檔案的最後一次修改時間相同來判斷是否使用快取。程式碼如下:

 1 var handle = function(req, res) {
 2     fs.stat(filename, function(err, stat){
 3         var lastModified = stat.mtime.toUTCString();
 4         if (lastModified === req.headers['if-modified-since']) {
 5             res.writeHead(304, "Not Modified");
 6             res.end();
 7         } else {
 8             fs.readFile(filename, function(err, file){
 9                 var lastModified = stat.mtime.toUTCString();
10                 res.setHeader("Last-Modified", lastModified);
11                 res.writeHead(200, "OK");
12                 res.end(file);
13             });
14         }
15     })
16 }

3. Expires 和 Cache-Control

以上兩種的快取判斷都需要客戶端向服務端先傳送一個條件請求,根據返回來決定是否使用快取。需要一定的時間開銷和頻寬。而實際上瀏覽器最先判斷的是Expires 和 Cache-Control。在服務端相應裡設定Expires 或 Cache-Control,瀏覽器會根據該值進行快取。Expires是一個GMT格式的時間字串。瀏覽器再接收到這個過期值後,只要本地還存在對應快取檔案,在到期時間之前它都不會再發起請求。但它的缺陷是瀏覽器與伺服器之間的時間可能不一致,導致檔案提前過期或已經過期卻還沒刪除。

而Cache-Control恰恰解決了這個問題:

1 var handle = function(req, res) {
2     fs.readFile(filename, function(err, file){
3         res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60);
4         res.writeHead(200, "OK");
5         res.end(file);
6     });
7 }

上面的程式碼為Cache-Control設定了max-age值為10年,max-age會告訴瀏覽器檔案多長時間後過期,進行倒數計時式的計算。這樣就可以避免客戶端與伺服器端時間不一致帶來的問題了。此外Cache-Control還可以public、private、no-cache、no-store等更精細地控制快取的選項。HTTP1.0時還不支援max-age,如今的服務端在模組的支援下多半同時對Expires 和 Cache-Control進行支援,如果瀏覽器中兩個值都存在且同時被支援,max-age會覆蓋Expires。

這兩種方法雖然節省了頻寬和請求時間,但其缺陷是當服務端的檔案內容進行了更新時,無法通知客戶端更新。因為瀏覽器是根據URL進行快取的,所以我們一般在對靜態資源使用快取時也會對其設定版本號。使得客戶端能請求到新的內容。一般更新機制有如下兩種方式:

  • 每次釋出,web應用或靜態資源的路徑中附帶對應的版本號:http://url.com/?v=20141216
  • 每次釋出,web應用或靜態資源的路徑中附帶檔案內容的hash碼:http://url.com/?hash=sdasd4d

因為檔案內容更新並不意味著新的版本。所以使用hash值得方式會更加妥當一些。

相關文章