網(wǎng)上有很多關(guān)于 HTPP Cache 的知識,但個人感覺大部分講的并不好,而這個主題對于 Web 開發(fā)者來說很重要,其實假如想全面了解相關(guān)知識,參考 MDN 和 Google 開發(fā)者網(wǎng)站 這兩篇文章就可以了。千萬不要去看 RFC 文檔,個人在看的時候非常費勁,最后放棄了。
本文算一個筆記,闡述其中的關(guān)鍵點,也會說明為什么這個主題不好講(大部分寫的不好的原因在于整體把控做的不好,或者說理解的比較片面)。
要區(qū)分理解 Private browser caches 和 Shared proxy caches
HTTP Cache 包括瀏覽器緩存和代理服務(wù)器緩存(比如 CDN),很多文章在描述的時候沒有有效的區(qū)分這兩者,所以會讓人比較困惑。
瀏覽器緩存的服務(wù)架構(gòu)可能是這樣的:瀏覽器(Cache)=>服務(wù)器。
代理服務(wù)器緩存架構(gòu)可能是這樣的:瀏覽器=>CDN(Cache)=>源服務(wù)器。
不同的 HTTP Cache 解決的問題和使用的場景是不一樣的。個人理解瀏覽器緩存主要是為了避免不必要的請求和大量的網(wǎng)絡(luò)傳輸,而代理服務(wù)器緩存主要是為了讓服務(wù)離用戶更近更有效率(當(dāng)然也解決了請求和網(wǎng)絡(luò)傳輸)。
對于 Web 開發(fā)者來說,可能經(jīng)常遇到的還是瀏覽器緩存,這篇文章主要說的也是此類緩存。
而對于 HTTP Cache Header 指令(主要是 Cache-Control)來說,對于這兩種類型的緩存,具體在使用上有不少的區(qū)別,需要仔細分辨。
瀏覽器行為是不可控的
HTTP Cache 是通過 HTTP Cache Header 指令來控制的,指令分為請求和響應(yīng)指令,響應(yīng)指令告訴瀏覽器應(yīng)該做什么(當(dāng)然瀏覽器可以不遵守),而響應(yīng)指令也一定程度上控制未來可能的請求指令。
不過不同瀏覽器針對請求指令處理機制可能是不一樣的,比如瀏覽器“回退動作”、“F5 動作”、“Ctrl +F5” 等動作會發(fā)出不一樣的請求指令。
具體查看下面的圖,通過這張圖,就明白為什么“回退動作”,會從瀏覽器緩存獲取數(shù)據(jù)了。

使用 HTTP/1.1 標準的指令
HTTP 協(xié)議是一直演變的,不考慮瀏覽器版本和服務(wù)器的問題,盡量使用最新標準協(xié)議的頭,因為假如混著理解,會讓人很困惑,比如 Expires 和 Pargma 等指令都可以被 Cache-Control 指令替代了。
正確理解 Cache-Control 指令
這個指令是一個通用首部字段,就是說這個指令能夠作為請求和響應(yīng)指令,同時這個指令的參數(shù)也有多個,比如說其參數(shù) max-age = 0 在請求和響應(yīng)指令中分別代表什么?在理解的時候一定要分辨清楚。
進一步理解 Cache-Control 指令
理解了這個指令基本上就理解了 HTTP Cache,個人覺得這句話(Cache-Control directives control who can cache the response, under which conditions, and for how long)精確描述了這個指令。
它有三個含義:
(1)能否緩存(針對響應(yīng)來說)
- private:表示它只應(yīng)該存在與瀏覽器緩存。
- public:表示它可以緩存在瀏覽器或者 CDN 上。
- no-cache:這個詞很迷惑,不是代表不能使用緩存,而是代表在使用前必須到服務(wù)器上確認。
- no-store:表示不允許被緩存。
(2)緩存多久(針對響應(yīng)來說)
- max-age= 秒,告知瀏覽器這個緩存的有效時間多少。
(3)revalidation(針對響應(yīng)來說,就是條件檢查)
- must-revalidate:表示瀏覽器必須檢查服務(wù)器,確認本地緩存是否有效,這個參數(shù)和請求參數(shù) max-age = 0 有些類似。
這個指令形象的告訴瀏覽器,你是不是可以緩存這個對象,這個對象緩存時間是多少,是否在每次使用緩存的時候先確認下。
如何使用你的 Cache-Control 策略
對于一個開發(fā)者來說,如何設(shè)定Cache-Control 策略是門藝術(shù),首先要明白資源是什么性質(zhì)的,在此基礎(chǔ)上定義 HTTP Cache 策略,Google 開發(fā)者網(wǎng)站的這張圖形象的描述了策略。

- 這個資源是否允許緩存?
- 客戶端每次使用緩存的時候需要去服務(wù)器校驗嗎?
- 這個緩存是 Public 的還是 Private?
- 緩存時間多少?
- 資源標識符是什么(Etag)?
瀏覽器如何校驗緩存
通過上面的描述,開發(fā)者明白了如何設(shè)置 HTTP Cache ,那么瀏覽器如何選擇是否使用緩存呢?理解了這個會鞏固理解 Cache-Control 策略。
這里面會增加兩個指令,ETag 指令和 Last-modified,分別代表什么含義呢?ETag 表示資源的唯一性,假如這個值變化了代表資源更新了;Last-modified 表示資源最后的更新時間;
通過上面的圖也可以發(fā)現(xiàn),開發(fā)者可以在響應(yīng)的時候輸出這兩個頭信息。那這個指令代表什么意思呢?
當(dāng)瀏覽器發(fā)現(xiàn)本地有緩存,且服務(wù)器指示沒有必要每次使用前去確認,那么瀏覽器可以直接使用本地緩存。
當(dāng)瀏覽器發(fā)現(xiàn)緩存已經(jīng)過期了,那么這個時候可以選擇重新去獲取資源,但是有這么一種情況,服務(wù)器資源其實沒有變化,那么為了減少帶寬使用,服務(wù)器輸出一個 304 HTTP 協(xié)議頭,告訴瀏覽器,你繼續(xù)使用你存儲的緩存把。
問題來了,服務(wù)器怎么知道這個資源沒有變化呢(從瀏覽器緩存生效的那時算),假如在第一次響應(yīng)的時候輸出了 Last-modified 頭(表示資源的最后更新時間),那么客戶端發(fā)現(xiàn)緩存失效的時候,在請求的時候會帶上 if-Modified-Since(其實就是 Last-modified 的值)信息,服務(wù)器一看服務(wù)器上的資源最后更新時間小于或等于 if-Modified-Since 時間,就表示這個資源其實是新的,然后就發(fā)送一個 304 頭。
如何通過 Nginx 來配置
大部分情況下,Nginx 會進行如下配置,但是需要明白含義,
location ~* \.(ico|css|js|gif|jpe?g|png)(\?[0-9]+)?$ {
expires 10d;
}
expires 這個指定會輸出如下的頭:
Cache-Control:max-age=864000
Date:Tue, 28 Mar 2017 10:00:38 GMT
ETag:"5864a0ab-1e75"
Expires:Fri, 07 Apr 2017 10:00:38 GMT
Last-Modified:Thu, 29 Dec 2016 05:35:39 GMT
假如緩存沒有過期就會一直使用,每次也不會去服務(wù)器校驗,假如想每次請求資源的時候都確認下,可以使用以下指令:
location ~* \.(ico|css|js|gif|jpe?g|png)(\?[0-9]+)?$ {
expires 10d;
add_header Cache-Control "no-cache,must-revalidate,max-age=0";
}
動態(tài)程序如何控制
動態(tài)程序要負責(zé)所有的 HTTP Cache 頭輸出,還要自己計算 Etag,直接上代碼看把:
<?php
$now = gmdate("D, d M Y H:i:s", time() ) . " GMT";
$if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false;
if ($if_modified_since && $if_modified_since >$now){
header('HTTP/1.1 304 Not Modified');
exit();
} else {
$seconds_to_cache = 3600*24;
$ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
header("Last-Modified: $ts");
header("Cache-Control: no-cache, must-revalidate");
}