輕鬆理解HTTP快取策略
阿新 • • 發佈:2020-12-21
上一篇文章我寫了[koa-static的原始碼解析](https://www.cnblogs.com/dennisj/p/14096348.html),其中用到了`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`裡面**,可能長這樣:
```javascript
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
```
客戶端拿到後會將這個`ETag`和返回值一起存下來,等下次請求時,使用配套的`If-None-Match`,將這個**放到`request`的`header`裡面**,可能長這樣:
```javascript
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
```
然後服務端拿到請求裡面的`If-None-Match`跟當前版本的`ETag`比較下:
1. 如果是一樣的話,直接返回`304`,語義為`Not Modified`,不返回內容(`body`),只返回`header`,告訴瀏覽器直接用快取。
2. 如果不一樣的話,返回`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`裡面的**,可能長這樣:
```javascript
Last-Modified: Wed, 21 Oct 2000 07:28:00 GMT
```
而客戶端瀏覽器在使用時,應該將配套的`If-Modified-Since`放到**`request`的`header`裡面**,長這樣:
```javascript
If-Modified-Since: Wed, 21 Oct 2000 07:28:00 GMT
```
服務端拿到這個頭後,會跟當前版本的修改時間進行比較:
1. 當前版本的修改時間比這個晚,也就是這個時間後又改過了,返回`200`和新的內容
2. 當前版本的修改時間和這個一樣,也就是沒有更新,返回`304`,不返回內容,只返回頭,客戶端直接使用快取
與`If-Modified-Since`對應的還有`If-Unmodified-Since`,`If-Modified-Since`可以理解為**有更新才下載**,那`If-Unmodified-Since`就是**沒有更新才下載**。如果客戶端傳了`If-Unmodified-Since`,像這樣:
```javascript
If-Unmodified-Since: Wed, 21 Oct 2000 07:28:00 GMT
```
服務端拿到這個頭後,也會跟當前版本的修改時間進行比較:
1. 如果這個時間後沒有更新,伺服器返回`200`,並返回內容。
2. 如果這個時間後有更新,其實就是這個`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`帶上這個欄位:
```javascript
Expires: Wed, 21 Oct 2000 07:28:00 GMT
```
然後在這個時間前,客戶端瀏覽器都不會再發起請求,而是直接用快取資源。
#### Cache-Control
`Cache-Control`相對比較複雜,可設定屬性也比較多,`max-age`只是其中一個屬性,長這樣:
```javascript
Cache-Control: max-age=20000
```
這表示當前資源在`20000秒`內都不用再請求了,直接使用快取。
上面提到的`immutable`也是`Cache-Control`的一個屬性,但是是個實驗性質的,各個瀏覽器相容並不好。設定了`Cache-control: immutable`表示這輩子都用快取了,再請求是不可能的了。
其他常用屬性還有:
`no-cache`:使用快取前,強制要求把請求提交給伺服器進行驗證(協商快取驗證)。
`no-store`:不儲存有關客戶端請求或伺服器響應的任何內容,即不使用任何快取。
另外`Cache-Control`還有很多屬性,大家可以參考[MDN的文件](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)。
#### Expires和Cache-Control的優先順序
就一句話:如果在`Cache-Control`響應頭設定了 `max-age` 或者 `s-maxage` 指令,那麼 `Expires` 頭會被忽略。
### 協商快取和強制快取優先順序
這個其實很好理解,協商快取需要發請求跟伺服器協商,強制快取如果生效,根本就不會發請求。所以這個優先順序就是:**先判斷強制快取,如果強制快取生效,直接使用快取;如果強制快取失效,再發請求跟伺服器協商,看要不要使用快取**。
## 總結
本文從生活中常見的場景入手,闡述了`HTTP`快取機制其實是提高訪問速度和解決資訊不同步的一種機制。這種資訊不同步在生活中很常見,很多解決思路我們已經司空見慣,帶著這種思維,我們可以很好的理解`HTTP`快取機制。`HTTP`快取機制要點如下:
1. `HTTP`快取機制分為**強制快取**和**協商快取**兩類。
2. **強制快取**的意思就是不要問了(不發起請求),直接用快取吧。
3. **強制快取**常見技術有`Expires`和`Cache-Control`。
4. `Expires`的值是一個時間,表示這個時間前快取都有效,都不需要發起請求。
5. `Cache-Control`有很多屬性值,常用屬性`max-age`設定了快取有效的時間長度,單位為`秒`,這個時間沒到,都不用發起請求。
6. `immutable`也是`Cache-Control`的一個屬性,表示這個資源這輩子都不用再請求了,但是他相容性不好,`Cache-Control`其他屬性可以參考[MDN的文件](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)。
7. `Cache-Control`的`max-age`優先順序比`Expires`高。
8. **協商快取**常見技術有`ETag`和`Last-Modified`。
9. `ETag`其實就是給資源算一個`hash`值或者版本號,對應的常用`request header`為`If-None-Match`。
10. `Last-Modified`其實就是加上資源修改的時間,對應的常用`request header`為`If-Modified-Since`,精度為`秒`。
11. `ETag`每次修改都會改變,而`Last-Modified`的精度只到`秒`,所以`ETag`更準確,優先順序更高,但是需要計算,所以服務端開銷更大。
12. **強制快取**和**協商快取**都存在的情況下,先判斷**強制快取**是否生效,如果生效,不用發起請求,直接用快取。如果**強制快取**不生效再發起請求判斷**協商快取**。
## 參考資料:
`ETag MDN`文件:[https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag](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](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](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](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
**文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。**
**作者博文GitHub專案地址: [https://github.com/dennis-jiang/Front-End-Knowledges](https://github.com/dennis-jiang/Front-End-Knowledges)**
**作者文章彙總:[https://juejin.im/post/5e3ffc85518825494e2772fd](https://juejin.im/post/5e3ffc85518825494e2772fd)**
**我也搞了個公眾號[進擊的大前端],可以第一時間獲取高質量原創,歡迎關注~**