上一篇文章我寫了koa-static的原始碼解析,其中用到了HTTP
的快取策略,給返回的靜態檔案設定了一些快取的頭,比如Cache-Control
之類的。於是我就跟朋友討論了一下HTTP
的快取策略:
朋友說:“HTTP
裡面控制快取的頭(header
)太多了,啥Cache-Control
,ETag
,Last-Modified
,一大堆,亂七八糟的,而且之間邏輯關係不強,要掌握基本靠背!”
我有點驚訝:“為什麼要去背這個呢?所有的技術都是為了解決問題而存在的,不瞭解問題而去單純的學習技術,去,背,去,死記,確實很枯燥,而且效果不好。HTTP
快取策略只是為了解決客戶端和服務端資訊不對稱的問題而存在的,客戶端為了加快速度會快取部分資源,但是下次請求時,客戶端不知道這個資源有沒有更新,服務端也不知道客戶端快取的是哪個版本,不知道該不該再返回資源,其實就是一個資訊同步問題,HTTP
快取策略就是來解決這個問題的。如果我們跳出這種純粹的技術思維,我們會發現生活中這種資訊同步問題也很常見。而我們解決這些問題的思路很多時候都是司空見慣了,如果從這個角度來說,這個問題就很好理解!”
於是我給他講了一個我小時候租光碟看奧特曼的故事。
租光碟看奧特曼
事情是這樣的,我小時候特別喜歡看動畫片,尤其是奧特曼,但是那時候沒有電腦啊,也沒有網路。我只有一臺DVD播放機,於是我會經常跑去租光碟的店租奧特曼。
ETag
某天,我看完了《艾斯奧特曼》第10集,我還想繼續看。於是我找到了光碟店的老闆:“老闆,第10集我看完了哦,你還有沒有新的啊?”老闆說:“有有有,剛出了第11集,你拿去吧!”
上面這一個簡單的交流過程其實就包含了一個HTTP
的快取技術,那就是ETag
!類比於網路請求,我其實就是客戶端,光碟店就是服務端,我去租光碟就相當於發起一個請求。但是我去租光碟時,老闆並不知道我看到哪集了,我們的資訊是不同步的。所以我告訴了他一個標記(Tag
),在這裡這個標記就是第10集,老闆拿到這個標記,跟他自己庫存的標記比較一下,發現他最新標記是第11集,於是知道有更新了,將第11集給了我。
Last-Modified & If-Modified-Since
再來,我《艾斯奧特曼》看完了,我開始看《泰羅奧特曼》了。可是老闆這次比較雞賊,《泰羅奧特曼》沒買正版的,是他自己翻錄的,他翻錄的時候自己也不知道是第幾集,但是他聰明的在光碟上寫上了翻錄日期。於是我正在看的這盤也沒啥封面,只光禿禿的寫了一個2000年12月1日。當我這盤看完了,我又去找老闆了:“老闆,你這個2000年12月1日的我已經看完了,你還有沒有新的啊?”這裡的2000年12月1日其實就是標記了我手上副本的更新日期,這也對應了HTTP
的一個快取技術,那就是Last-Modified
和If-Modified-Since
。你可以理解為,老闆給日期還取了一個名字,叫Last-Modified
,所以光碟上完整文字是Last-Modified:2000年12月1日
,而我去問的時候就這麼問:“Do you have any updates IF-Modified-Since 2000年12月1日?”。
Expires和Max-Age
繼續,我《泰羅奧特曼》也看完了,開始看《雷歐奧特曼》了。這《雷歐奧特曼》跟前面兩個都不一樣,我去租的時候老闆就說了:“你小子別天天跑來問了!《雷歐奧特曼》我每週去進一次貨,你每週一來拿就行!”這句話也對應了一個HTTP
快取技術,那就是Expires
和Max-Age
。我知道了下週一之前,我手上都是最新的,到了下週一就過期(Expire
)了。所以“我手上的是最新的”這個說法有個生命週期,他的年齡是有限的,他的年齡等於下週一更新時間減去當前時間,這就是他的最大年齡(Max-Age
)。
Immutable
再來一個,我《雷歐奧特曼》也看完了,開始看《奈克斯特奧特曼》了。這《奈克斯特奧特曼》跟前面幾個都不一樣,我去租的時候老闆說了:“小子,你這次運氣好,這《奈克斯特奧特曼》已經出完了,你全部拿去吧,也不用天天跑來問了!”這句話對應的HTTP
快取技術是啥?當然是Immutable!Immutable
就跟字面意思一樣,不可變的!就像《奈克斯特奧特曼》一樣,已經出完了,不用再去問更新了。
言歸正傳
扯蛋到這裡結束,我們們言歸正傳!之所以舉這麼個例子,是為了說明HTTP
快取技術要解決的問題在生活中很常見,從這些常見的場景入手,理解起來更簡單。下面我們正兒八經的來說說HTTP
快取技術:
兩種機制
從上面的幾個小例子可以看出,有時候為了知道是不是有更新,我必須去問老闆,比如第一個例子裡面:“老闆,第10集我看完了哦,你還有沒有新的啊?”。這種為了知道有沒有更新,必須跟服務端溝通過才知道的,我們稱之為協商快取。還有些場景,我不去問就知道有沒有更新,比如第三個例子,因為知道是周更的,當週一來之前,我都不會去問了,到了週一再去問,這種不用跟伺服器協商直接用本地副本的叫做強制快取。換成技術的話說就是,強制快取不用發請求直接用本地快取,協商快取要發請求去問伺服器有沒有更新。下面我們詳細來講下這兩種快取:
協商快取
前面第一個例子和第二個例子每次都需要向伺服器端詢問,所以是協商快取。
ETag和If-None-Match
ETag
是URL的Entity Tag
,就是一個URL資源的識別符號,類似於檔案的md5
,計算方式也類似,當伺服器返回時,可以根據返回內容計算一個hash
值或者就是一個數字版本號,類似於我們的第10集
,具體返回什麼值要看伺服器的計算策略。然後將它加到response
的header
裡面,可能長這樣:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
客戶端拿到後會將這個ETag
和返回值一起存下來,等下次請求時,使用配套的If-None-Match
,將這個放到request
的header
裡面,可能長這樣:
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
然後服務端拿到請求裡面的If-None-Match
跟當前版本的ETag
比較下:
- 如果是一樣的話,直接返回
304
,語義為Not Modified
,不返回內容(body
),只返回header
,告訴瀏覽器直接用快取。 - 如果不一樣的話,返回
200
和最新的內容
與ETag
配套的還有一個不太常用的request header
----If-Match
,這個和前面If-None-Match
的語義是相反的。前面If-None-Match
的語義是如果不匹配就下載。而If-Match
通常用於post
或者put
請求中,語義為如果匹配才提交,比如你在編輯一個商品,其他人也可能同時在編輯。當你提交編輯時,其他人可能已經先於你提交了,這時候服務端的ETag
就已經變了,If-Match
就不成立了,這時候服務端會給你返回412
錯誤,也就是Precondition Failed
,前提條件失敗。如果If-Match
成立,就正常返回200
。
Last-Modified & If-Modified-Since
Last-Modified
和If-Modified-Since
也是配套使用的,類似於ETag
和If-None-Match
的關係。只不過ETag
放的是一個版本號或者hash
值,Last-Modified
放的是資源的最後修改時間。Last-Modified
是放到response
的header
裡面的,可能長這樣:
Last-Modified: Wed, 21 Oct 2000 07:28:00 GMT
而客戶端瀏覽器在使用時,應該將配套的If-Modified-Since
放到request
的header
裡面,長這樣:
If-Modified-Since: Wed, 21 Oct 2000 07:28:00 GMT
服務端拿到這個頭後,會跟當前版本的修改時間進行比較:
- 當前版本的修改時間比這個晚,也就是這個時間後又改過了,返回
200
和新的內容 - 當前版本的修改時間和這個一樣,也就是沒有更新,返回
304
,不返回內容,只返回頭,客戶端直接使用快取
與If-Modified-Since
對應的還有If-Unmodified-Since
,If-Modified-Since
可以理解為有更新才下載,那If-Unmodified-Since
就是沒有更新才下載。如果客戶端傳了If-Unmodified-Since
,像這樣:
If-Unmodified-Since: Wed, 21 Oct 2000 07:28:00 GMT
服務端拿到這個頭後,也會跟當前版本的修改時間進行比較:
- 如果這個時間後沒有更新,伺服器返回
200
,並返回內容。 - 如果這個時間後有更新,其實就是這個
if
不成立,會返回錯誤程式碼412
,語義為Precondition Failed
ETag和Last-Modified優先順序
ETag
和Last-Modified
都是協商快取,都需要伺服器進行計算和比較,那如果這兩個都存在,用哪個呢?答案是ETag
,ETag
的優先順序比Last-Modified
高。因為Last-Modified
在設計上有個問題,那就是Last-Modified
的精度只能到秒,如果一個資源頻繁修改,在同一秒進行多次修改,你從Last-Modified
上是看不出來區別的。但是ETag
每次修改都會生成新的,所以他比Last-Modified
精度高,更準確。但是ETag
也不是完全沒問題的,你的ETag
如果設計為一個hash
值,每次請求都要計算這個值,需要額外耗費伺服器資源。具體使用哪一個,需要根據自己的專案情況來進行取捨。
強制快取
上面扯蛋那裡的第三個例子和第四個例子就是強制快取,就是我知道在某個時間段完全不用去問服務端,直接去用快取就行。這兩個例子裡面提到的Expires
是一個單獨的header
,max-age
和immutable
同屬於Cache-Control
這個header
。
Expires
Expires
比較簡單,就是伺服器response
的header
帶上這個欄位:
Expires: Wed, 21 Oct 2000 07:28:00 GMT
然後在這個時間前,客戶端瀏覽器都不會再發起請求,而是直接用快取資源。
Cache-Control
Cache-Control
相對比較複雜,可設定屬性也比較多,max-age
只是其中一個屬性,長這樣:
Cache-Control: max-age=20000
這表示當前資源在20000秒
內都不用再請求了,直接使用快取。
上面提到的immutable
也是Cache-Control
的一個屬性,但是是個實驗性質的,各個瀏覽器相容並不好。設定了Cache-control: immutable
表示這輩子都用快取了,再請求是不可能的了。
其他常用屬性還有:
no-cache
:使用快取前,強制要求把請求提交給伺服器進行驗證(協商快取驗證)。
no-store
:不儲存有關客戶端請求或伺服器響應的任何內容,即不使用任何快取。
另外Cache-Control
還有很多屬性,大家可以參考MDN的文件。
Expires和Cache-Control的優先順序
就一句話:如果在Cache-Control
響應頭設定了 max-age
或者 s-maxage
指令,那麼 Expires
頭會被忽略。
協商快取和強制快取優先順序
這個其實很好理解,協商快取需要發請求跟伺服器協商,強制快取如果生效,根本就不會發請求。所以這個優先順序就是:先判斷強制快取,如果強制快取生效,直接使用快取;如果強制快取失效,再發請求跟伺服器協商,看要不要使用快取。
總結
本文從生活中常見的場景入手,闡述了HTTP
快取機制其實是提高訪問速度和解決資訊不同步的一種機制。這種資訊不同步在生活中很常見,很多解決思路我們已經司空見慣,帶著這種思維,我們可以很好的理解HTTP
快取機制。HTTP
快取機制要點如下:
HTTP
快取機制分為強制快取和協商快取兩類。- 強制快取的意思就是不要問了(不發起請求),直接用快取吧。
- 強制快取常見技術有
Expires
和Cache-Control
。 Expires
的值是一個時間,表示這個時間前快取都有效,都不需要發起請求。Cache-Control
有很多屬性值,常用屬性max-age
設定了快取有效的時間長度,單位為秒
,這個時間沒到,都不用發起請求。immutable
也是Cache-Control
的一個屬性,表示這個資源這輩子都不用再請求了,但是他相容性不好,Cache-Control
其他屬性可以參考MDN的文件。Cache-Control
的max-age
優先順序比Expires
高。- 協商快取常見技術有
ETag
和Last-Modified
。 ETag
其實就是給資源算一個hash
值或者版本號,對應的常用request header
為If-None-Match
。Last-Modified
其實就是加上資源修改的時間,對應的常用request header
為If-Modified-Since
,精度為秒
。ETag
每次修改都會改變,而Last-Modified
的精度只到秒
,所以ETag
更準確,優先順序更高,但是需要計算,所以服務端開銷更大。- 強制快取和協商快取都存在的情況下,先判斷強制快取是否生效,如果生效,不用發起請求,直接用快取。如果強制快取不生效再發起請求判斷協商快取。
參考資料:
ETag MDN
文件:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag
Last-Modified MDN
文件:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Last-Modified
Expires MDN
文件:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Expires
Cache-Control MDN
文件:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。
作者博文GitHub專案地址: https://github.com/dennis-jiang/Front-End-Knowledges
作者文章彙總:https://juejin.im/post/5e3ffc85518825494e2772fd
我也搞了個公眾號[進擊的大前端],可以第一時間獲取高質量原創,歡迎關注~