圖解 HTTP 緩存
點擊上方“服務(wù)端思維”,選擇“設(shè)為星標(biāo)”
回復(fù)”669“獲取獨家整理的精選資料集
回復(fù)”加群“加入全國服務(wù)端高端社群「后端圈」

前言
HTTP 的緩存機(jī)制,可以說這是前端工程師需要掌握的重要知識點之一。本文將針對 HTTP 緩存整體的流程做一個詳細(xì)的講解,爭取做到大家讀完整篇文章后,對緩存有一個整體的了解。
HTTP 緩存分為 2 種,一種是強(qiáng)緩存,另一種是協(xié)商緩存。主要作用是可以加快資源獲取速度,提升用戶體驗,減少網(wǎng)絡(luò)傳輸,緩解服務(wù)端的壓力。這是緩存運(yùn)作的一個整體流程圖:
Http緩存.jpg強(qiáng)緩存
不需要發(fā)送請求到服務(wù)端,直接讀取瀏覽器本地緩存,在 Chrome 的 Network 中顯示的 HTTP 狀態(tài)碼是 200 ,在 Chrome 中,強(qiáng)緩存又分為 Disk Cache(存放在硬盤中)和 Memory Cache(存放在內(nèi)存中),存放的位置是由瀏覽器控制的。是否強(qiáng)緩存由 Expires、Cache-Control 和 Pragma 3 個 Header 屬性共同來控制。
○ Expires
Expires 的值是一個 HTTP 日期,在瀏覽器發(fā)起請求時,會根據(jù)系統(tǒng)時間和 Expires 的值進(jìn)行比較,如果系統(tǒng)時間超過了 Expires 的值,緩存失效。由于和系統(tǒng)時間進(jìn)行比較,所以當(dāng)系統(tǒng)時間和服務(wù)器時間不一致的時候,會有緩存有效期不準(zhǔn)的問題。Expires 的優(yōu)先級在三個 Header 屬性中是最低的。
○ Cache-Control
Cache-Control 是 HTTP/1.1 中新增的屬性,在請求頭和響應(yīng)頭中都可以使用,常用的屬性值如有:
- max-age:單位是秒,緩存時間計算的方式是距離發(fā)起的時間的秒數(shù),超過間隔的秒數(shù)緩存失效
- no-cache:不使用強(qiáng)緩存,需要與服務(wù)器驗證緩存是否新鮮
- no-store:禁止使用緩存(包括協(xié)商緩存),每次都向服務(wù)器請求最新的資源
- private:專用于個人的緩存,中間代理、CDN 等不能緩存此響應(yīng)
- public:響應(yīng)可以被中間代理、CDN 等緩存
- must-revalidate:在緩存過期前可以使用,過期后必須向服務(wù)器驗證
○ Pragma
Pragma 只有一個屬性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用強(qiáng)緩存,需要與服務(wù)器驗證緩存是否新鮮,在 3 個頭部屬性中的優(yōu)先級最高。
本地通過 express 起一個服務(wù)來驗證強(qiáng)緩存的 3 個屬性,代碼如下:const?express?=?require('express');
const?app?=?express();
var?options?=?{?
??etag:?false,?//?禁用協(xié)商緩存
??lastModified:?false,?//?禁用協(xié)商緩存
??setHeaders:?(res,?path,?stat)?=>?{
????res.set('Cache-Control',?'max-age=10');?//?強(qiáng)緩存超時時間為10秒
??},
};
app.use(express.static((__dirname?+?'/public'),?options));
app.listen(3000);
第一次加載,頁面會向服務(wù)器請求數(shù)據(jù),并在 Response Header 中添加 Cache-Control ,過期時間為 10 秒。
緩存1.jpg第二次加載,Date 頭屬性未更新,可以看到瀏覽器直接使用了強(qiáng)緩存,實際沒有發(fā)送請求。
緩存2.jpg過了 10 秒的超時時間之后,再次請求資源:
緩存3.jpg當(dāng) Pragma 和 Cache-Control 同時存在的時候,Pragma 的優(yōu)先級高于 Cache-Control。
緩存5.jpg協(xié)商緩存
當(dāng)瀏覽器的強(qiáng)緩存失效的時候或者請求頭中設(shè)置了不走強(qiáng)緩存,并且在請求頭中設(shè)置了If-Modified-Since 或者 If-None-Match 的時候,會將這兩個屬性值到服務(wù)端去驗證是否命中協(xié)商緩存,如果命中了協(xié)商緩存,會返回 304 狀態(tài),加載瀏覽器緩存,并且響應(yīng)頭會設(shè)置 Last-Modified 或者 ETag 屬性。
○ ETag/If-None-Match
ETag/If-None-Match 的值是一串 hash 碼,代表的是一個資源的標(biāo)識符,當(dāng)服務(wù)端的文件變化的時候,它的 hash碼會隨之改變,通過請求頭中的 If-None-Match 和當(dāng)前文件的 hash 值進(jìn)行比較,如果相等則表示命中協(xié)商緩存。ETag 又有強(qiáng)弱校驗之分,如果 hash 碼是以 "W/" 開頭的一串字符串,說明此時協(xié)商緩存的校驗是弱校驗的,只有服務(wù)器上的文件差異(根據(jù) ETag 計算方式來決定)達(dá)到能夠觸發(fā) hash 值后綴變化的時候,才會真正地請求資源,否則返回 304 并加載瀏覽器緩存。
○ Last-Modified/If-Modified-Since
Last-Modified/If-Modified-Since 的值代表的是文件的最后修改時間,第一次請求服務(wù)端會把資源的最后修改時間放到 Last-Modified 響應(yīng)頭中,第二次發(fā)起請求的時候,請求頭會帶上上一次響應(yīng)頭中的 Last-Modified 的時間,并放到 If-Modified-Since 請求頭屬性中,服務(wù)端根據(jù)文件最后一次修改時間和 If-Modified-Since 的值進(jìn)行比較,如果相等,返回 304 ,并加載瀏覽器緩存。
本地通過 express 起一個服務(wù)來驗證協(xié)商緩存,代碼如下:const?express?=?require('express');
const?app?=?express();
var?options?=?{?
??etag:?true,?//?開啟協(xié)商緩存
??lastModified:?true,?//?開啟協(xié)商緩存
??setHeaders:?(res,?path,?stat)?=>?{
????res.set({
??????'Cache-Control':?'max-age=00',?//?瀏覽器不走強(qiáng)緩存
??????'Pragma':?'no-cache',?//?瀏覽器不走強(qiáng)緩存
????});
??},
};
app.use(express.static((__dirname?+?'/public'),?options));
app.listen(3001);
第一次請求資源:
緩存6.jpg第二次請求資源,服務(wù)端根據(jù)請求頭中的 If-Modified-Since 和 If-None-Match 驗證文件是否修改。
緩存7.jpg我們再來驗證一下 ETag 在強(qiáng)校驗的情況下,只增加一行空格,hash 值如何變化,在代碼中,我采用的是對文件進(jìn)行 MD5 加密來計算其 hash 值。
注:只是為了演示用,實際計算不是通過 MD5 加密的,Apache 默認(rèn)通過 FileEtag 中 FileEtag INode Mtime Size 的配置自動生成 ETag,用戶可以通過自定義的方式來修改文件生成 ETag 的方式。
為了保證 lastModified 不影響緩存,我把通過 Last-Modified/If-Modified-Since 請求頭刪除了,源碼如下:const?express?=?require('express');
const?CryptoJS?=?require('crypto-js/crypto-js');
const?fs?=?require('fs');
const?app?=?express();
var?options?=?{?
??etag:?true,?//?只通過Etag來判斷
??lastModified:?false,?//?關(guān)閉另一種協(xié)商緩存
??setHeaders:?(res,?path,?stat)?=>?{
????const?data?=?fs.readFileSync(path,?'utf-8');?//?讀取文件
????const?hash?=?CryptoJS.MD5((JSON.stringify(data)));?//?MD5加密
????res.set({
??????'Cache-Control':?'max-age=00',?//?瀏覽器不走強(qiáng)緩存
??????'Pragma':?'no-cache',?//?瀏覽器不走強(qiáng)緩存
??????'ETag':?hash,?//?手動設(shè)置Etag值為MD5加密后的hash值
????});
??},
};
app.use(express.static((__dirname?+?'/public'),?options));
app.listen(4000);?//?使用新端口號,否則上面驗證的協(xié)商緩存會一直存在
第一次和第二次請求如下:
緩存10.jpg
緩存11.jpg然后我修改了 test.js ,增加一個空格后再刪除一個空格,保持文件內(nèi)容不變,但文件的修改時間改變,發(fā)起第三次請求,由于我生成 ETag 的方式是通過對文件內(nèi)容進(jìn)行 MD5 加密生成,所以雖然修改時間變化了,但請求依然返回了 304,讀取瀏覽器緩存。
緩存13.jpgETag/If-None-Match 的出現(xiàn)主要解決了 Last-Modified/If-Modified-Since 所解決不了的問題:
- 如果文件的修改頻率在秒級以下,Last-Modified/If-Modified-Since 會錯誤地返回 304
- 如果文件被修改了,但是內(nèi)容沒有任何變化的時候,Last-Modified/If-Modified-Since 會錯誤地返回 304,上面的例子就說明了這個問題
總結(jié)
在實際使用場景中,比如政采云的官網(wǎng)。圖片、不常變化的 JS 等靜態(tài)資源都會使用緩存來提高頁面的加載速度。例如政采云首頁的頂部導(dǎo)航欄,埋點 SDK 等等。
在文章的最后,我們再次回到這張流程圖,這張圖涵蓋了 HTTP 緩存的整體流程,大家對整體流程熟悉后,也可以自己動手通過 Node 來驗證下 HTTP 緩存。
Http緩存.jpg— 本文結(jié)束 —

●?漫談設(shè)計模式在 Spring 框架中的良好實踐
●?顛覆微服務(wù)認(rèn)知:深入思考微服務(wù)的七個主流觀點
●?要黑盒測試微服務(wù)內(nèi)部服務(wù)間調(diào)用,我該如何實現(xiàn)?
關(guān)注我,回復(fù) 「加群」 加入各種主題討論群。
對「服務(wù)端思維」有期待,請在文末點個在看
喜歡這篇文章,歡迎轉(zhuǎn)發(fā)、分享朋友圈
在看點這里
