HTTP 協議的緩存機制涉及到多個請求頭字段,而且整個緩存機制的細節行為也存在各種情況的差異,譬如說什么時候訪問本地緩存不發送請求,什么時候發送請求查看資源是否更新,獲取 response 什么情況下更新緩存等。以前我對此一知半解只是籠統的知道一些概念,譬如 Cache-Control 可以控制緩存的時間和是否需要緩存,但是緩存過期后的行為,有緩存后瀏覽器是否有 http 請求都不甚了解。所以特地 google 下,此篇是對此的知識梳理。
協議概述
什么情況下可以使用本地緩存?譬如說我們用 get 方式請求了一個資源 http://mytest.domain.com/static/images/bg.png
,那么我們下次再請求這個圖片資源的時候符合哪些條件可以使用本地緩存呢?
- url 必須是
http://mytest.domain.com/static/images/bg.png
,如果是http://mytest.domain.com/static/images/bg.png?t=12312321
就會發起新的請求,因為 url 不同。 - 發送請求的 method 必須可被緩存,譬如 get。
- 第一次請求 response(即本地緩存)如果有一個 Vary 頭,他的值列出的是一系列 http header,第二次請求的請求頭中那些在 Vary 值中所列的頭,必須和第一次請求相同(具體規則)。
- 第二次請求不包含請求頭 Pragma: no-cache
- 第二次請求不包含請求頭 Cache-Control: no-cache|max-age=0
- 第一次請求的 response(即本地緩存)不包含 Cache-Control: no-cache
- 本地緩存沒有過期
- 或者雖然本地緩存已經過期,但是服務器驗證緩存和服務器資源一致,允許使用本地緩存的情況(即獲得304 response)
注1:上述任何提一個條件都可以被 cache-control extension 覆蓋
注2:response header中不僅僅可以 Cache-Control: no-cache,還可以 Cache-Control: no-cache="Set-Cookie" 詳見
如何計算本地緩存是否過期
瀏覽器是通過比較緩存剩余有效時間和當前緩存已存在時間來判斷的:response_is_fresh = (freshness_lifetime > current_age)
。freshness_lifetime
取值優先級次序如下列表所示(排在上面的優先級越高):
- Cache-Control: s-maxage=xx
- Cache-Control: max-age=xx
- Expires: xxxxx
- 按規則進行計算(推測)
注1;如果有多個重復的上述頭,那么是非法的,視作 response(資源)過期
freshness_lifetime 計算(推測)規則:
規范并沒有給出具體的算法,但是給出了最壞情況(but does impose worst-case constraints on their results),如果 response(資源)有 Last-Modified 頭,那么推薦用從當前到lastmodified這個時間段的 10% 作為 freshness_lifetime
,并且 response(資源)的 current_age 如果已經超過24小時,必須在這個 response 上加上113 warn-code頭。很少有瀏覽器實現了freshness_lifetime
的自助計算(推測),所以還是還是鼓勵給出上述顯式的1 - 3三種情況。
current_age 計算規則:
泛泛來說就是 response(資源)在本地的駐足時間加上網絡傳輸時間,因為網絡傳輸時間的計算有多個條件,規范實在看的我頭暈,所以具體計算規則詳見規范
本地緩存過期,如何通過服務器驗證緩存的是否依然有效(即304的情況)
首先,如果第一次的 response(資源)頭中顯示申明了一些禁止緩存的頭(譬如:"no-store" or "no-cache" 等等),就不存在過期不過期的問題,因為這個資源不允許緩存。其次,過期緩存也不一定不可用,如果在斷網或者第二次請求帶上 max-stale 這個請求頭,那么瀏覽器可以使用過期的緩存(masx-stale 表示在緩存過期后多少時間內瀏覽器依然可以使用緩存)。碰到過期緩存,瀏覽器可以發送一個條件請求(conditional request)。這個請求的 url 依然是第一次請求的 url,只是會帶上些當前資源的一些信息,以供服務器驗證這個緩存是否依然可用還是需要更新:
- response(資源)的 Last-Modified 頭所帶的值會放到條件請求的 If-Modified-Since 頭中,或者是 If-Unmodified-Since 又或者 If-Range。
- response(資源)的 ETag 頭所帶的值會放到條件請求的 If-None-Match 頭中,或者 If-Match 又或者 If-Range。
服務器會根據不同的條件請求頭來驗證資源,具體的行為詳見規范,這里不細致展開。針對條件請求,服務器返回會有三種情況:
- 一個帶有304 status code 的返回。表示緩存可以被更新和重用。
- 一個帶有 body 的完整的 response。表示用這個 response 作為請求的返回,并且視條件可以用這個完整的 response 替換瀏覽器原有的緩存(此資源)。
- 如果返回一個5xx的 response,那么瀏覽器可以選擇就顯示這個5xx的返回,或者使用本地緩存(盡管可能是過期的)- 規范沒有規定應該選擇哪種處理,應該是取決于瀏覽器的行為。
如果是一個304的返回,規范說這個返回可以更新本地緩存,更新策略分三種:
- 如果這個304 response 帶有資源有效性的強驗證頭,那么瀏覽器會尋找本地緩存,尋找那些帶有同樣強驗證頭的緩存,然后用這個最新的 response 去更新這些匹配的緩存(同一個資源可能瀏覽器保存有多份緩存,譬如日期不同等)。
- 如果這個304 response 帶有資源有效性的弱驗證頭,那么瀏覽器同樣會找相匹配的緩存,但是只會更新最新的那條匹配的緩存。
- 如果這個304 response 沒有帶有任何資源有效性驗證頭,并且瀏覽器緩存只有一份,并且這份也同樣沒帶有任何資源有效性驗證頭,那么瀏覽器就會用這個304 response,更新本地緩存。
協議流程圖
假設第一次請求一個資源,返回 header 里面帶上如下字段:
Cache-Control: max-age=600
Last-Modified: Wed, 28 Aug 2013 10:36:42 GMT
ETag: "124752e0d85461a16e76fbdef2e84fb9"
拋開細枝末節的東西,那么第二次請求通常大致流程圖如下:
當前資源緩存是否過期:response_is_fresh = (freshness_lifetime > current_age)
|
-----------------------------------
| |
是 否
| |
發送請求,帶上請求頭 從本地緩存中獲取資源(不發請求)
If-Modified-Since: 此資源Last-Modified的值
If-None-Match: 此資源ETag的值
|
服務器根據 If-Modified-Since 和 If-None-Match
兩個值判斷資源是否更新過
|
-------------------------
| |
是 否
| |
返回一個 status code:200 的 response 返回一個 status code:304 的 response
response body 里面是請求的資源 response body 為空
| |
瀏覽器用 response body里面的資源 依然從本地緩存里面獲取資源
替換本地緩存中的資源
特殊情況
當你去瀏覽器驗證的時候可能會碰到一些特殊情況,就是緩存有效,但是你刷新瀏覽器依然發送的條件請求。其實是因為瀏覽器在請求頭中加入了一些料,譬如: Cache-Control: max-age=0。你刷新的方式可以有很多種,譬如:按F5,按ctrl+F5,在地址欄按回車等等。這些不同的行為都會影響瀏覽器發送請求的行為。這里有一些參考《在瀏覽器地址欄按回車、F5、Ctrl+F5刷新網頁的區別》