天天看點

輕松了解HTTP緩存政策

上一篇文章我寫了koa-static的源碼解析,其中用到了<code>HTTP</code>的緩存政策,給傳回的靜态檔案設定了一些緩存的頭,比如<code>Cache-Control</code>之類的。于是我就跟朋友讨論了一下<code>HTTP</code>的緩存政策:

朋友說:“<code>HTTP</code>裡面控制緩存的頭(<code>header</code>)太多了,啥<code>Cache-Control</code>,<code>ETag</code>,<code>Last-Modified</code>,一大堆,亂七八糟的,而且之間邏輯關系不強,要掌握基本靠背!”

我有點驚訝:“為什麼要去背這個呢?所有的技術都是為了解決問題而存在的,不了解問題而去單純的學習技術,去,背,去,死記,确實很枯燥,而且效果不好。<code>HTTP</code>緩存政策隻是為了解決用戶端和服務端資訊不對稱的問題而存在的,用戶端為了加快速度會緩存部分資源,但是下次請求時,用戶端不知道這個資源有沒有更新,服務端也不知道用戶端緩存的是哪個版本,不知道該不該再傳回資源,其實就是一個資訊同步問題,<code>HTTP</code>緩存政策就是來解決這個問題的。如果我們跳出這種純粹的技術思維,我們會發現生活中這種資訊同步問題也很常見。而我們解決這些問題的思路很多時候都是司空見慣了,如果從這個角度來說,這個問題就很好了解!”

于是我給他講了一個我小時候租光碟看奧特曼的故事。

事情是這樣的,我小時候特别喜歡看卡通片,尤其是奧特曼,但是那時候沒有電腦啊,也沒有網絡。我隻有一台DVD播放機,于是我會經常跑去租光碟的店租奧特曼。

某天,我看完了《艾斯奧特曼》第10集,我還想繼續看。于是我找到了光碟店的老闆:“老闆,第10集我看完了哦,你還有沒有新的啊?”老闆說:“有有有,剛出了第11集,你拿去吧!”

上面這一個簡單的交流過程其實就包含了一個<code>HTTP</code>的緩存技術,那就是<code>ETag</code>!類比于網絡請求,我其實就是用戶端,光碟店就是服務端,我去租光碟就相當于發起一個請求。但是我去租光碟時,老闆并不知道我看到哪集了,我們的資訊是不同步的。是以我告訴了他一個标記(<code>Tag</code>),在這裡這個标記就是第10集,老闆拿到這個标記,跟他自己庫存的标記比較一下,發現他最新标記是第11集,于是知道有更新了,将第11集給了我。

再來,我《艾斯奧特曼》看完了,我開始看《泰羅奧特曼》了。可是老闆這次比較雞賊,《泰羅奧特曼》沒買正版的,是他自己翻錄的,他翻錄的時候自己也不知道是第幾集,但是他聰明的在CD光牒上寫上了翻錄日期。于是我正在看的這盤也沒啥封面,隻光秃秃的寫了一個2000年12月1日。當我這盤看完了,我又去找老闆了:“老闆,你這個2000年12月1日的我已經看完了,你還有沒有新的啊?”這裡的2000年12月1日其實就是标記了我手上副本的更新日期,這也對應了<code>HTTP</code>的一個緩存技術,那就是<code>Last-Modified</code>和<code>If-Modified-Since</code>。你可以了解為,老闆給日期還取了一個名字,叫<code>Last-Modified</code>,是以光碟上完整文字是<code>Last-Modified:2000年12月1日</code>,而我去問的時候就這麼問:“Do you have any updates IF-Modified-Since 2000年12月1日?”。

繼續,我《泰羅奧特曼》也看完了,開始看《雷歐奧特曼》了。這《雷歐奧特曼》跟前面兩個都不一樣,我去租的時候老闆就說了:“你小子别天天跑來問了!《雷歐奧特曼》我每周去進一次貨,你每周一來拿就行!”這句話也對應了一個<code>HTTP</code>緩存技術,那就是<code>Expires</code>和<code>Max-Age</code>。我知道了下周一之前,我手上都是最新的,到了下周一就過期(<code>Expire</code>)了。是以“我手上的是最新的”這個說法有個生命周期,他的年齡是有限的,他的年齡等于下周一更新時間減去目前時間,這就是他的最大年齡(<code>Max-Age</code>)。

再來一個,我《雷歐奧特曼》也看完了,開始看《奈克斯特奧特曼》了。這《奈克斯特奧特曼》跟前面幾個都不一樣,我去租的時候老闆說了:“小子,你這次運氣好,這《奈克斯特奧特曼》已經出完了,你全部拿去吧,也不用天天跑來問了!”這句話對應的<code>HTTP</code>緩存技術是啥?當然是Immutable!<code>Immutable</code>就跟字面意思一樣,不可變的!就像《奈克斯特奧特曼》一樣,已經出完了,不用再去問更新了。

扯蛋到這裡結束,咱們言歸正傳!之是以舉這麼個例子,是為了說明<code>HTTP</code>緩存技術要解決的問題在生活中很常見,從這些常見的場景入手,了解起來更簡單。下面我們正兒八經的來說說<code>HTTP</code>緩存技術:

從上面的幾個小例子可以看出,有時候為了知道是不是有更新,我必須去問老闆,比如第一個例子裡面:“老闆,第10集我看完了哦,你還有沒有新的啊?”。這種為了知道有沒有更新,必須跟服務端溝通過才知道的,我們稱之為協商緩存。還有些場景,我不去問就知道有沒有更新,比如第三個例子,因為知道是周更的,當周一來之前,我都不會去問了,到了周一再去問,這種不用跟伺服器協商直接用本地副本的叫做強制緩存。換成技術的話說就是,強制緩存不用發請求直接用本地緩存,協商緩存要發請求去問伺服器有沒有更新。下面我們詳細來講下這兩種緩存:

前面第一個例子和第二個例子每次都需要向伺服器端詢問,是以是協商緩存。

<code>ETag</code>是URL的<code>Entity Tag</code>,就是一個URL資源的辨別符,類似于檔案的<code>md5</code>,計算方式也類似,當伺服器傳回時,可以根據傳回内容計算一個<code>hash</code>值或者就是一個數字版本号,類似于我們的<code>第10集</code>,具體傳回什麼值要看伺服器的計算政策。然後将它加到<code>response</code>的<code>header</code>裡面,可能長這樣:

用戶端拿到後會将這個<code>ETag</code>和傳回值一起存下來,等下次請求時,使用配套的<code>If-None-Match</code>,将這個放到<code>request</code>的<code>header</code>裡面,可能長這樣:

然後服務端拿到請求裡面的<code>If-None-Match</code>跟目前版本的<code>ETag</code>比較下:

如果是一樣的話,直接傳回<code>304</code>,語義為<code>Not Modified</code>,不傳回内容(<code>body</code>),隻傳回<code>header</code>,告訴浏覽器直接用緩存。

如果不一樣的話,傳回<code>200</code>和最新的内容

與<code>ETag</code>配套的還有一個不太常用的<code>request header</code>----<code>If-Match</code>,這個和前面<code>If-None-Match</code>的語義是相反的。前面<code>If-None-Match</code>的語義是如果不比對就下載下傳。而<code>If-Match</code>通常用于<code>post</code>或者<code>put</code>請求中,語義為如果比對才送出,比如你在編輯一個商品,其他人也可能同時在編輯。當你送出編輯時,其他人可能已經先于你送出了,這時候服務端的<code>ETag</code>就已經變了,<code>If-Match</code>就不成立了,這時候服務端會給你傳回<code>412</code>錯誤,也就是<code>Precondition Failed</code>,前提條件失敗。如果<code>If-Match</code>成立,就正常傳回<code>200</code>。

<code>Last-Modified</code>和<code>If-Modified-Since</code>也是配套使用的,類似于<code>ETag</code>和<code>If-None-Match</code>的關系。隻不過<code>ETag</code>放的是一個版本号或者<code>hash</code>值,<code>Last-Modified</code>放的是資源的最後修改時間。<code>Last-Modified</code>是放到<code>response</code>的<code>header</code>裡面的,可能長這樣:

而用戶端浏覽器在使用時,應該将配套的<code>If-Modified-Since</code>放到<code>request</code>的<code>header</code>裡面,長這樣:

服務端拿到這個頭後,會跟目前版本的修改時間進行比較:

目前版本的修改時間比這個晚,也就是這個時間後又改過了,傳回<code>200</code>和新的内容

目前版本的修改時間和這個一樣,也就是沒有更新,傳回<code>304</code>,不傳回内容,隻傳回頭,用戶端直接使用緩存

與<code>If-Modified-Since</code>對應的還有<code>If-Unmodified-Since</code>,<code>If-Modified-Since</code>可以了解為有更新才下載下傳,那<code>If-Unmodified-Since</code>就是沒有更新才下載下傳。如果用戶端傳了<code>If-Unmodified-Since</code>,像這樣:

服務端拿到這個頭後,也會跟目前版本的修改時間進行比較:

如果這個時間後沒有更新,伺服器傳回<code>200</code>,并傳回内容。

如果這個時間後有更新,其實就是這個<code>if</code>不成立,會傳回錯誤代碼<code>412</code>,語義為<code>Precondition Failed</code>

<code>ETag</code>和<code>Last-Modified</code>都是協商緩存,都需要伺服器進行計算和比較,那如果這兩個都存在,用哪個呢?答案是<code>ETag</code>,<code>ETag</code>的優先級比<code>Last-Modified</code>高。因為<code>Last-Modified</code>在設計上有個問題,那就是<code>Last-Modified</code>的精度隻能到秒,如果一個資源頻繁修改,在同一秒進行多次修改,你從<code>Last-Modified</code>上是看不出來差別的。但是<code>ETag</code>每次修改都會生成新的,是以他比<code>Last-Modified</code>精度高,更準确。但是<code>ETag</code>也不是完全沒問題的,你的<code>ETag</code>如果設計為一個<code>hash</code>值,每次請求都要計算這個值,需要額外耗費伺服器資源。具體使用哪一個,需要根據自己的項目情況來進行取舍。

上面扯蛋那裡的第三個例子和第四個例子就是強制緩存,就是我知道在某個時間段完全不用去問服務端,直接去用緩存就行。這兩個例子裡面提到的<code>Expires</code>是一個單獨的<code>header</code>,<code>max-age</code>和<code>immutable</code>同屬于<code>Cache-Control</code>這個<code>header</code>。

<code>Expires</code>比較簡單,就是伺服器<code>response</code>的<code>header</code>帶上這個字段:

然後在這個時間前,用戶端浏覽器都不會再發起請求,而是直接用緩存資源。

<code>Cache-Control</code>相對比較複雜,可設定屬性也比較多,<code>max-age</code>隻是其中一個屬性,長這樣:

這表示目前資源在<code>20000秒</code>内都不用再請求了,直接使用緩存。

上面提到的<code>immutable</code>也是<code>Cache-Control</code>的一個屬性,但是是個實驗性質的,各個浏覽器相容并不好。設定了<code>Cache-control: immutable</code>表示這輩子都用緩存了,再請求是不可能的了。

其他常用屬性還有:

<code>no-cache</code>:使用緩存前,強制要求把請求送出給伺服器進行驗證(協商緩存驗證)。

<code>no-store</code>:不存儲有關用戶端請求或伺服器響應的任何内容,即不使用任何緩存。

另外<code>Cache-Control</code>還有很多屬性,大家可以參考MDN的文檔。

就一句話:如果在<code>Cache-Control</code>響應頭設定了 <code>max-age</code> 或者 <code>s-maxage</code> 指令,那麼 <code>Expires</code> 頭會被忽略。

這個其實很好了解,協商緩存需要發請求跟伺服器協商,強制緩存如果生效,根本就不會發請求。是以這個優先級就是:先判斷強制緩存,如果強制緩存生效,直接使用緩存;如果強制緩存失效,再發請求跟伺服器協商,看要不要使用緩存。

本文從生活中常見的場景入手,闡述了<code>HTTP</code>緩存機制其實是提高通路速度和解決資訊不同步的一種機制。這種資訊不同步在生活中很常見,很多解決思路我們已經司空見慣,帶着這種思維,我們可以很好的了解<code>HTTP</code>緩存機制。<code>HTTP</code>緩存機制要點如下:

<code>HTTP</code>緩存機制分為強制緩存和協商緩存兩類。

強制緩存的意思就是不要問了(不發起請求),直接用緩存吧。

強制緩存常見技術有<code>Expires</code>和<code>Cache-Control</code>。

<code>Expires</code>的值是一個時間,表示這個時間前緩存都有效,都不需要發起請求。

<code>Cache-Control</code>有很多屬性值,常用屬性<code>max-age</code>設定了緩存有效的時間長度,機關為<code>秒</code>,這個時間沒到,都不用發起請求。

<code>immutable</code>也是<code>Cache-Control</code>的一個屬性,表示這個資源這輩子都不用再請求了,但是他相容性不好,<code>Cache-Control</code>其他屬性可以參考MDN的文檔。

<code>Cache-Control</code>的<code>max-age</code>優先級比<code>Expires</code>高。

協商緩存常見技術有<code>ETag</code>和<code>Last-Modified</code>。

<code>ETag</code>其實就是給資源算一個<code>hash</code>值或者版本号,對應的常用<code>request header</code>為<code>If-None-Match</code>。

<code>Last-Modified</code>其實就是加上資源修改的時間,對應的常用<code>request header</code>為<code>If-Modified-Since</code>,精度為<code>秒</code>。

<code>ETag</code>每次修改都會改變,而<code>Last-Modified</code>的精度隻到<code>秒</code>,是以<code>ETag</code>更準确,優先級更高,但是需要計算,是以服務端開銷更大。

強制緩存和協商緩存都存在的情況下,先判斷強制緩存是否生效,如果生效,不用發起請求,直接用緩存。如果強制緩存不生效再發起請求判斷協商緩存。

<code>ETag MDN</code>文檔:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag

<code>Last-Modified MDN</code>文檔:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Last-Modified

<code>Expires MDN</code>文檔:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Expires

<code>Cache-Control MDN</code>文檔:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝啬你的贊和GitHub小星星,你的支援是作者持續創作的動力。

歡迎關注我的公衆号進擊的大前端第一時間擷取高品質原創~

“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端進階知識”系列文章源碼GitHub位址: https://github.com/dennis-jiang/Front-End-Knowledges

輕松了解HTTP緩存政策

繼續閱讀