淺談瀏覽器http的緩存機制

針對瀏覽器的http緩存的分析也算是老生常談了,每隔一段時間就會冒出一篇不錯的文章,其原理也是各大公司面試時幾乎必考的問題。css

之因此還寫一篇這樣的文章,是由於近期都在搞新技術,想「迴歸」下基礎,也但願儘可能總結的更詳盡些。html

那麼你是否還須要閱讀本篇文章呢?能夠試着回答下面這個問題:前端

咱們在訪問百度首頁的時候,會發現無論怎麼刷新頁面,靜態資源基本都是返回 200(from cache)nginx

隨便點開一個靜態資源是醬的:面試

哎喲有Response報頭數據呢,看來服務器也正常返回了etag什麼鬼的應有盡有,那狀態200不是應該對應的非緩存狀態麼?要from cache的話不是應該返回304才合理麼?ajax

難道是度孃的服務器故障了嗎?算法

若是你知道答案,那就能夠忽略本文了。chrome

http報文中與緩存相關的首部字段瀏覽器

咱們先來瞅一眼RFC2616規定的47種http報文首部字段中與緩存相關的字段,事先了解一下能讓咱在內心有個底:緩存

1. 通用首部字段(就是請求報文和響應報文都能用上的字段)

2. 請求首部字段

3. 響應首部字段

4. 實體首部字段

後續大致也會依次介紹它們。

場景模擬

爲方便模擬各類緩存效果,咱們建個很是簡單的場景。

1. 頁面文件

咱們建個很是簡單的html頁面,上面只有一個本地樣式文件和圖片:

複製代碼
<!DOCTYPE html>
<html>
<head>
<title>緩存測試</title>
<link rel="stylesheet" href="css/reset.css">
</head>
<body>
<h1>哥只是一個標題</h1>
<p><img src="img/dog.jpg" /></p>
</body>
</html>
複製代碼

2. 首部字段修改

有時候一些瀏覽器會自行給請求首部加上一些字段(如chrome使用F5會強制加上「cache-control:max-age=0」),會覆蓋掉一些字段(好比pragma)的功能;另外有時候咱們但願服務器能多/少返回一些響應字段。

這種狀況咱們就但願能夠手動來修改請求或響應報文上的內容了。那麼如何實現呢?這裏咱們使用Fiddler來完成任務。

在Fiddler中咱們能夠經過「bpu XXX」指令來攔截指定請求,而後手動修改請求內容再發給服務器、修改響應內容再發給客戶端。

以咱們的example爲例,頁面文件走nginx經過 http://localhost/ 可直接訪問,因此咱們直接執行「bpu localhost」攔截全部地址中帶有該字樣的請求:

點擊被攔截的請求,能夠在右欄直接修改報文內容(上半區域是請求報文,下半區域是響應報文),點擊黃色的「Break on Response」按鈕能夠執行下一步(把請求發給服務器),點擊綠色的按鈕「Run to Completion」能夠直接完成整個請求過程:

經過這個方法咱們能夠很輕鬆地模擬出各類http緩存場景。

3. 瀏覽器的強制策略

如上述,當下大多數瀏覽器在點擊刷新按鈕或按F5時會自行加上「Cache-Control:max-age=0」請求字段,因此咱們先約定成俗——後文說起的「刷新」多指的是選中url地址欄並按回車鍵(這樣不會被強行加上Cache-Control)

事實上有的瀏覽器還有一些更奇怪的行爲,在後續咱們回答文章開頭問題的時候會提到。

石器時代的緩存方式

在 http1.0 時代,給客戶端設定緩存方式可經過兩個字段——「Pragma」和「Expires」來規範。雖然這兩個字段早可拋棄,但爲了作http協議的向下兼容,你仍是能夠看到不少網站依舊會帶上這兩個字段。

1. Pragma

當該字段值爲「no-cache」的時候(事實上如今RFC中也僅標明該可選值),會知會客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行。

Pragma屬於通用首部字段,在客戶端上使用時,常規要求咱們往html上加上這段meta元標籤(並且可能還得作些hack放到body後面去):

<meta http-equiv="Pragma" content="no-cache">

它告訴瀏覽器每次請求頁面時都不要讀緩存,都得往服務器發一次請求才行。

BUT!!! 事實上這種禁用緩存的形式用處頗有限:

1. 僅有IE才能識別這段meta標籤含義,其它主流瀏覽器僅能識別「Cache-Control: no-store」的meta標籤(見出處
2. 在IE中識別到該meta標籤含義,並不必定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)

作了測試後發現也的確如此,這種客戶端定義Pragma的形式基本沒起到多少做用。

不過若是是在響應報文上加上該字段就不同了:

如上圖紅框部分是再次刷新頁面時生成的請求,這說明禁用緩存生效,預計瀏覽器在收到服務器的Pragma字段後會對資源進行標記,禁用其緩存行爲,進然後續每次刷新頁面均能從新發出請求而不走緩存。

2. Expires

有了Pragma來禁用緩存,天然也須要有個東西來啓用緩存和定義緩存時間,對http1.0而言,Expires就是作這件事的首部字段。

Expires的值對應一個GMT(格林尼治時間),好比「Mon, 22 Jul 2002 11:12:01 GMT」來告訴瀏覽器資源緩存過時時間,若是還沒過該時間點則不發請求。

在客戶端咱們一樣可使用meta標籤來知會IE(也僅有IE能識別)頁面(一樣也只對頁面有效,對頁面上的資源無效)緩存時間:

<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

若是但願在IE下頁面不走緩存,但願每次刷新頁面都能發新請求,那麼能夠把「content」裏的值寫爲「-1」或「0」。

注意的是該方式僅僅做爲知會IE緩存時間的標記,你並不能在請求或響應報文中找到Expires字段。

若是是在服務端報頭返回Expires字段,則在任何瀏覽器中都能正確設置資源緩存的時間:

在上圖裏,緩存時間設置爲一個已過時的時間點(見紅框),則刷新頁面將從新發送請求(見藍框)

那麼若是Pragma和Expires一塊兒上陣的話,聽誰的?咱們試一試就知道了:

咱們經過Pragma禁用緩存,又給Expires定義一個還未到期的時間(紅框),刷新頁面時發現均發起了新請求(藍框),這意味着Pragma字段的優先級會更高。

BUT,響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,若是客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了本身電腦的系統時間),那緩存時間可能就沒啥意義了。

Cache-Control

針對上述的「Expires時間是相對服務器而言,沒法保證和客戶端時間統一」的問題,http1.1新增了 Cache-Control 來定義緩存過時時間,若報文中同時出現了 Pragma、Expires 和 Cache-Control,會以 Cache-Control 爲準。

Cache-Control也是一個通用首部字段,這意味着它能分別在請求報文和響應報文中使用。在RFC中規範了 Cache-Control 的格式爲:

"Cache-Control" ":" cache-directive

做爲請求首部時,cache-directive 的可選值有:

做爲響應首部時,cache-directive 的可選值有:

咱們依舊能夠在HTML頁面加上meta標籤來給請求報頭加上 Cache-Control 字段:

另外 Cache-Control 容許自由組合可選值,例如:

Cache-Control: max-age=3600, must-revalidate

它意味着該資源是從原服務器上取得的,且其緩存(新鮮度)的有效時間爲一小時,在後續一小時內,用戶從新訪問該資源則無須發送請求。

固然這種組合的方式也會有些限制,好比 no-cache 就不能和 max-age、min-fresh、max-stale 一塊兒搭配使用。

組合的形式還能作一些瀏覽器行爲不一致的兼容處理。例如在IE咱們可使用 no-cache 來防止點擊「後退」按鈕時頁面資源從緩存加載,但在 Firefox 中,須要使用 no-store 才能防止歷史回退時瀏覽器不從緩存中去讀取數據,故咱們在響應報頭加上以下組合值便可作兼容處理:

Cache-Control: no-cache, no-store

緩存校驗字段

上述的首部字段均能讓客戶端決定是否向服務器發送請求,好比設置的緩存時間未過時,那麼天然直接從本地緩存取數據便可(在chrome下表現爲200 from cache),若緩存時間過時了或資源不應直接走緩存,則會發請求到服務器去。

咱們如今要說的問題是,若是客戶端向服務器發了請求,那麼是否意味着必定要讀取回該資源的整個實體內容呢?

咱們試着這麼想——客戶端上某個資源保存的緩存時間過時了,但這時候其實服務器並無更新過這個資源,若是這個資源數據量很大,客戶端要求服務器再把這個東西從新發一遍過來,是否很是浪費帶寬和時間呢?

答案是確定的,那麼是否有辦法讓服務器知道客戶端如今存有的緩存文件,其實跟本身全部的文件是一致的,而後直接告訴客戶端說「這東西你直接用緩存裏的就能夠了,我這邊沒更新過呢,就再也不傳一次過去了」。

爲了讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提高緩存的複用率,Http1.1新增了幾個首部字段來作這件事情。

1. Last-Modified

服務器將資源傳遞給客戶端時,會將資源最後更改的時間以「Last-Modified: GMT」的形式加在實體首部上一塊兒返回給客戶端。

客戶端會爲資源標記上該信息,下次再次請求時,會把該信息附帶在請求報文中一併帶給服務器去作檢查,若傳遞的時間值與服務器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回304狀態碼便可。

至於傳遞標記起來的最終修改時間的請求報文首部字段一共有兩個:

⑴ If-Modified-Since: Last-Modified-value

示例爲  If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

該請求首部告訴服務器若是客戶端傳來的最後修改時間與服務器上的一致,則直接回送304 和響應報頭便可。

當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 Last-Modified 值。

 If-Unmodified-Since: Last-Modified-value

告訴服務器,若Last-Modified沒有匹配上(資源在服務端的最後更新時間改變了),則應當返回412(Precondition Failed) 狀態碼給客戶端。

當遇到下面狀況時,If-Unmodified-Since 字段會被忽略:

1. Last-Modified值對上了(資源在服務端沒有新的修改);
2. 服務端需返回2XX和412以外的狀態碼;
3. 傳來的指定日期不合法

Last-Modified 說好卻也不是特別好,由於若是在服務器上,一個資源被修改了,但其實際內容根本沒發生改變,會由於Last-Modified時間匹配不上而返回了整個實體給客戶端(即便客戶端緩存裏有個如出一轍的資源)

2. ETag

爲了解決上述Last-Modified可能存在的不許確的問題,Http1.1還推出了 ETag 實體首部字段。

服務器會經過某種算法,給資源計算得出一個惟一標誌符(好比md5標誌),在把資源響應給客戶端的時候,會在實體首部加上「ETag: 惟一標識符」一塊兒返回給客戶端。

客戶端會保留該 ETag 字段,並在下一次請求時將其一併帶過去給服務器。服務器只須要比較客戶端傳來的ETag跟本身服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。

若是服務器發現ETag匹配不上,那麼直接以常規GET 200回包形式將新的資源(固然也包括了新的ETag)發給客戶端;若是ETag是一致的,則直接返回304知會客戶端直接使用本地緩存便可。

那麼客戶端是如何把標記在資源上的 ETag 傳去給服務器的呢?請求報文中有兩個首部字段能夠帶上 ETag 值:

⑴ If-None-Match: ETag-value

示例爲  If-None-Match: "56fcccc8-1699"

告訴服務端若是 ETag 沒匹配上須要重發資源數據,不然直接回送304 和響應報頭便可。

當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 ETag 值。

⑵ If-Match: ETag-value

告訴服務器若是沒有匹配到ETag,或者收到了「*」值而當前並無該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。不然服務器直接忽略該字段。

If-Match 的一個應用場景是,客戶端走PUT方法向服務端請求上傳/更替資源,這時候能夠經過 If-Match 傳遞資源的ETag。

 

須要注意的是,若是資源是走分佈式服務器(好比CDN)存儲的狀況,須要這些服務器上計算ETag惟一值的算法保持一致,纔不會致使明明同一個文件,在服務器A和服務器B上生成的ETag卻不同。

若是 Last-Modified 和 ETag 同時被使用,則要求它們的驗證都必須經過纔會返回304,若其中某個驗證沒經過,則服務器會按常規返回資源實體及200狀態碼。

在較新的 nginx 上默認是同時開啓了這兩個功能的:

上圖的前三條請求是原始請求,接着的三條請求是刷新頁面後的新請求,在發新請求以前咱們修改了 reset.css 文件,因此它的 Last-Modified 和 ETag 均發生了改變,服務器所以返回了新的文件給客戶端(狀態值爲200)

而 dog.jpg 咱們沒有作修改,其Last-Modified 和 ETag在服務端是保持不變的,故服務器直接返回了304狀態碼讓客戶端直接使用緩存的 dog.jpg 便可,沒有把實體內容返回給客戶端(由於不必)

緩存實踐

當咱們在一個項目上作http緩存的應用時,咱們仍是會把上述說起的大多數首部字段均使用上,例如使用 Expires 來兼容舊的瀏覽器,使用 Cache-Control 來更精準地利用緩存,而後開啓 ETag 跟 Last-Modified 功能進一步複用緩存減小流量。

那麼這裏會有一個小問題——Expires 和 Cache-Control 的值應設置爲多少合適呢?

答案是不會有過於精準的值,均須要進行按需評估。

例如頁面連接的請求常規是無須作長時間緩存的,從而保證回退到頁面時能從新發出請求,百度首頁是用的 Cache-Control:private,騰訊首頁則是設定了60秒的緩存,即 Cache-Control:max-age=60。

而靜態資源部分,特別是圖片資源,一般會設定一個較長的緩存時間,並且這個時間最好是能夠在客戶端靈活修改的。以騰訊的某張圖片爲例:

http://i.gtimg.cn/vipstyle/vipportal/v4/img/common/logo.png?max_age=2592000

客戶端能夠經過給圖片加上「max_age」的參數來定義服務器返回的緩存時間:

固然這須要有一個前提——靜態資源能確保長時間不作改動。若是一個腳本文件響應給客戶端並作了長時間的緩存,而服務端在近期修改了該文件的話,緩存了此腳本的客戶端將沒法及時得到新的數據。

解決該困擾的辦法也簡單——把服務側ETag的那一套也搬到前端來用——頁面的靜態資源以版本形式發佈,經常使用的方法是在文件名或參數帶上一串md5或時間標記符:

https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg

若是文件被修改了,才更改其標記符內容,這樣能確保客戶端能及時從服務器收取到新修改的文件。

關於開頭的問題

如今回過頭來看文章開頭的問題,可能會以爲答案很容易回答出來。

百度首頁的資源在刷新後實際沒有發送任何請求,由於 Cache-Control 定義的緩存時間段還沒到期。在Chrome中即便沒發送請求,但只要從本地的緩存中取,都會在Network面板顯示一條狀態爲200且註明「from cache」的僞請求,其Response內容只是上一次回包留下的數據。

然而這並非問題的所有答案,咱們前面提到過,在Chrome中若是點擊「刷新」按鈕,Chrome會強制給全部資源加上「Cache-Control: max-age=0」的請求首部並向服務器發送驗證請求的,而在文章開頭的動圖中,咱們的確點擊了「刷新」按鈕,卻不見瀏覽器發去新請求(並返回304)

關於這個問題其實在組內跟小夥伴們討論過,經過Fiddler抓包發現,若是關閉Chrome的開發者面板再點擊「刷新」按鈕,瀏覽器是會按預期發送驗證請求且接收返回的304響應的,另外這個奇怪的狀況在不一樣的網站甚至不一樣的電腦下出現頻率都不一致,因此暫時將其歸咎於瀏覽器的怪異反應。

那麼有這麼一個問題——是否有辦法在瀏覽器點擊「刷新」按鈕的時候不讓瀏覽器去發新的驗證請求呢?

辦法仍是有的,就是不怎麼實用——在頁面加載完畢後經過腳本動態地添加資源:

複製代碼
$(window).load(function() {
      var bg='http://img.infinitynewtab.com/wallpaper/100.jpg';
      setTimeout(function() {  //setTimeout是必須的
       $('#bgOut').css('background-image', 'url('+bg+')');
      },0);
});
複製代碼

出處來自知乎,更具體的解釋能夠去看看。

其它相關的首部字段

事實上較經常使用和重要的緩存相關字段咱們都介紹完了,這裏順帶講講幾個跟緩存有關係,但沒那麼主要的響應首部字段。

1. Vary

「vary」自己是「變化」的意思,而在http報文中更趨因而「vary from」(與。。。不一樣)的含義,它表示服務端會以什麼基準字段來區分、篩選緩存版本。

咱們先考慮這麼一個問題——在服務端有着這麼一個地址,若是是IE用戶則返回針對IE開發的內容,不然返回另外一個主流瀏覽器版本的內容。這很簡單,服務端獲取到請求的 User-Agent 字段作處理便可。可是用戶請求的是代理服務器而非原服務器,且代理服務器若是直接把緩存的IE版本資源發給了非IE的客戶端,這就出問題了。

所以 Vary 即是着手處理該問題的首部字段,咱們能夠在響應報文加上:

Vary: User-Agent

便能知會代理服務器須要以 User-Agent 這個請求首部字段來區別緩存版本,防止傳遞給客戶端的緩存不正確。

Vary 也接受條件組合的形式:

Vary: User-Agent, Accept-Encoding

這意味着服務器應以 User-Agent 和 Accept-Encoding 兩個請求首部字段來區分緩存版本。

2. Date 和 Age

HTTP並無提供某種方法來幫用戶區分其收到的資源是否命中了代理服務器的緩存,但在客戶端咱們能夠經過計算響應報文中的 Date 和 Age 字段來獲得答案。

Date 理所固然是原服務器發送該資源響應報文的時間(GMT格式),若是你發現 Date 的時間與「當前時間」差異較大,或者連續F5刷新發現 Date 的值都沒變化,則說明你當前請求是命中了代理服務器的緩存。

上述的「當前時間」天然是相對於原服務器而言的時間,那麼如何獲悉原服務器的當前時間呢?

常規從頁面地址請求的響應報文中可得到,以博客園首頁爲例:

每次你刷新頁面,瀏覽器都會從新發出這條url的請求,你會發現其 Date 值是不斷變化的,這說明該連接沒有命中緩存,都是從原服務器返回過來的數據。

所以咱們能夠拿頁面上其它靜態資源請求回包中的 Date 與其進行對比,若靜態資源的 Date 早於原服務端時間,則說明命中了代理服務器緩存。

一般還知足這麼個條件:

靜態資源Age + 靜態資源Date = 原服務端Date

這裏的 Age 也是響應報文中的首部字段,它表示該文件在代理服務器中存在的時間(秒),如文件被修改或替換,Age會從新由0開始累計。

咱們在上面那張博客園首頁報文截圖的同個場景下,看看某個文件(jQuery.js)命中代理服務器緩存的回包數據:

會發現它知足咱們上述的規則:

//return true
new Date('Mon, 04 Apr 2016 07:03:17 GMT')/1000 == new Date('Sat, 19 Dec 2015 01:29:14 GMT')/1000 + 9264843

不過這條規則也不必定準確,特別是當原服務器常常修改系統時間的狀況下。

 

原文連接:http://www.cnblogs.com/vajoy/p/5341664.html

相關文章
相關標籤/搜索