前言
web緩存是高級前端工程師必修技能。是我們變成大牛過程中繞不開的知識點。
文章會儘量用通俗易懂的言語來細說web緩存的概念和用處。
本期文章的大綱是
什麼是web緩存(前端緩存)
緩存可以解決什麼問題?他的缺點是什麼?
強制緩存原理講解
3.1.基於Expires字段實現的強緩存
3.2.基於Cache-control實現的強緩存
協商緩存原理講解
4.1.基於last-modified實現的協商緩存
4.2.基於ETag實現的協商緩存
web緩存主要指的是兩部分:瀏覽器緩存和http緩存。
其中http緩存是web緩存的核心,是最難懂的那一部分,也是最重要的那一部分。
瀏覽器緩存:比如,localStorage,sessionStorage,cookie等等。這些功能主要用於緩存一些必要的數據,比如用戶信息。比如需要攜帶到後端的參數。亦或者是一些列表數據等等。
不過這裡需要注意。像localStorage,sessionStorage這種用戶緩存數據的功能,他只能保存5M左右的數據,多了不行。cookie則更少,大概只能有4kb的數據。不要擔心,這些概念對於未來會稱為前端大牛的你來說都不是什麼問題,非常的簡單。因為太簡單,數據緩存不再這篇文章的介紹中,這裡一筆帶過,需要了解的小夥伴,可以移步我的另一篇文章前端新能優化篇之localStorage和sessionStorage的區別及其使用方式 \- 掘金 \(juejin.cn\)[1]。
這篇文章重點講解的是:前端http緩存。
http緩存
官方介紹:Web 緩存是可以自動保存常見文檔副本的 HTTP 設備。當 Web 請求抵達緩存時, 如果本地有「已緩存的」副本,就可以從本地存儲設備而不是原始服務器中提取這 個文檔。
舉個例子↓

看圖,問題就是出在,服務器需要處理http的請求,並且http去傳輸數據,需要帶寬,帶寬是要錢買的啊。而我們緩存,就是為了讓服務器不去處理這個請求,客戶端也可以拿到數據。
注意,我們的緩存主要是針對html,css,img等靜態資源,常規情況下,我們不會去緩存一些動態資源,因為緩存動態資源的話,數據的實時性就不會不太好,所以我們一般都只會去緩存一些不太容易被改變的靜態資源。
緩存可以解決什麼問題?他的缺點是什麼?先說說,緩存可以解決什麼問題。
再說說缺點
其實日常的開發中,我們最最最最關心的,還是"更快的加載頁面";尤其是對於react/vue等SPA(單頁面)應用來說,首屏加載是老生常談的問題。這個時候,緩存就顯得非常重要。不需要往後端請求,直接在緩存中讀取。速度上,會有顯著的提升。是一種提升網站性能與用戶體驗的有效策略。
http緩存又分為兩種兩種緩存,強制緩存和協商緩存,我們來深度剖析一下強制緩存和協商緩存各自的優劣以及他們的使用場景以及使用原理
http緩存流程圖↓

強制緩存,我們簡稱強緩存。
從強制緩存的角度觸發,如果瀏覽器判斷請求的目標資源有效命中強緩存,如果命中,則可以直接從內存中讀取目標資源,無需與服務器做任何通訊。
基於Expires字段實現的強緩存在以前,我們通常會使用響應頭的Expires字段去實現強緩存。如下圖↓

Expires字段的作用是,設定一個強緩存時間。在此時間範圍內,則從內存(或磁盤)中讀取緩存返回。
比如說將某一資源設置響應頭為:Expires:new Date("2022-7-30 23:59:59");
那麼,該資源在2022-7-30 23:59:59 之前,都會去本地的磁盤(或內存)中讀取,不會去服務器請求。
但是,**Expires已經被廢棄了**。對於強緩存來說,Expires已經不是實現強緩存的首選。
因為Expires判斷強緩存是否過期的機制是:獲取本地時間戳,並對先前拿到的資源文件中的Expires字段的時間做比較。來判斷是否需要對服務器發起請求。這裡有一個巨大的漏洞:「如果我本地時間不準咋辦?」
是的,Expires過度依賴本地時間,如果本地與服務器時間不同步,就會出現資源無法被緩存或者資源永遠被緩存的情況。所以,Expires字段幾乎不被使用了。現在的項目中,我們並不推薦使用Expires,強緩存功能通常使用cache-control字段來代替Expires字段。
沒想到吧,整半天,這個屬性是廢的。

Cache-control這個字段在http1.1中被增加,Cache-control完美解決了Expires本地時間和服務器時間不同步的問題。是當下的項目中實現強緩存的最常規方法。
Cache-control的使用方法頁很簡單,只要在資源的響應頭上寫上需要緩存多久就好了,單位是秒。比如↓
//往響應頭中寫入需要緩存的時間res.writeHead(200,{'Cache-Control':'max-age=10'});複製代碼下圖的意思就是,從該資源第一次返回的時候開始,往後的10秒鐘內如果該資源被再次請求,則從緩存中讀取。
Cache-Control:max-age=N,N就是需要緩存的秒數。從第一次請求資源的時候開始,往後N秒內,資源若再次請求,則直接從磁盤(或內存中讀取),不與服務器做任何交互。
Cache-control中因為max-age後面的值是一個滑動時間,從服務器第一次返回該資源時開始倒計時。所以也就不需要比對客戶端和服務端的時間,解決了Expires所存在的巨大漏洞。
Cache-control有max-age、s-maxage、no-cache、no-store、private、public這六個屬性。
no_cache是Cache-control的一個屬性。它並不像字面意思一樣禁止緩存,實際上,no-cache的意思是強制進行協商緩存。如果某一資源的Cache-control中設置了no-cache,那麼該資源會直接跳過強緩存的校驗,直接去服務器進行協商緩存。而no-store就是禁止所有的緩存策略了。
注意,no-cache和no-store是一組互斥屬性,這兩個屬性不能同時出現在Cache-Control中。
public和private一般請求是從客戶端直接發送到服務端,如下↓

但有些情況下是例外的:比如,出現代理服務器,如下↓

而public和private就是決定資源是否可以在代理服務器進行緩存的屬性。
其中,public表示資源在客戶端和代理服務器都可以被緩存。
private則表示資源只能在客戶端被緩存,拒絕資源在代理服務器緩存。
如果這兩個屬性值都沒有被設置,則默認為private
注意,public和private也是一組互斥屬性。他們兩個不能同時出現在響應頭的cache-control字段中。
max-age和s-maxagemax-age表示的時間資源在客戶端緩存的時長,而s-maxage表示的是資源在代理服務器可以緩存的時長。
在一般的項目架構中max-age就夠用。
而s-maxage因為是代理服務端的緩存時長,他必須和上面說的public屬性一起使用(public屬性表示資源可以在代理服務器中緩存)。
注意,max-age和s-maxage並不互斥。他們可以一起使用。
那麼,Cache-control如何設置多個值呢?用逗號分割,如下↓
Cache-control:max-age=10000,s-maxage=200000,public
強制緩存就是以上這兩種方法了。現在我們回過頭來聊聊,Expires難道就一點用都沒有了嗎?也不是,雖然Cache-control是Expires的完全替代品,但是如果要考慮向下兼容的話,在Cache-control不支持的時候,還是要使用Expires,這也是我們當前使用的這個屬性的唯一理由。
協商緩存溫馨提示:協商緩存的內容會有一點點繞。需要仔細閱讀。
基於last-modified的協商緩存基於last-modified的協商緩存實現方式是:
三步缺一不可。
如下圖↓

注意圈出來的三行。
第一行,讀出修改時間。
第二行,給該資源響應頭的last-modified字段賦值修改時間
第三行,給該資源響應頭的Cache-Control字段值設置為:no-cache.(上文有介紹,Cache-control:no-cache的意思是跳過強緩存校驗,直接進行協商緩存。)
還沒完。到這裡還無法實現協商緩存
當客戶端讀取到last-modified的時候,會在下次的請求標頭中攜帶一個字段:If-Modified-Since。

而這個請求頭中的If-Modified-Since就是服務器第一次修改時候給他的時間,也就是上圖中的
這一行。
那麼之後每次對該資源的請求,都會帶上If-Modified-Since這個字段,而務端就需要拿到這個時間並再次讀取該資源的修改時間,讓他們兩個做一個比對來決定是讀取緩存還是返回新的資源。
如圖↓

這樣,就是協商緩存的所有操作了。
看到這裡,有些小夥伴可能有些迷糊了。

沒關係,我們用一張圖來解釋下協商緩存。

使用以上方式的協商緩存已經存在兩個非常明顯的漏洞。這兩個漏洞都是基於文件是通過比較修改時間來判斷是否更改而產生的。
1.因為是更具文件修改時間來判斷的,所以,在文件內容本身不修改的情況下,依然有可能更新文件修改時間(比如修改文件名再改回來),這樣,就有可能文件內容明明沒有修改,但是緩存依然失效了。
2.當文件在極短時間內完成修改的時候(比如幾百毫秒)。因為文件修改時間記錄的最小單位是秒,所以,如果文件在幾百毫秒內完成修改的話,文件修改時間不會改變,這樣,即使文件內容修改了,依然不會 返回新的文件。
為了解決上述的這兩個問題。從http1.1開始新增了一個頭信息,ETag(Entity 實體標籤)
又來新東西了,兄弟們頂住

不用太擔心,如果你已經理解了上面比較時間戳形式的協商緩存的話,ETag對你來說不會有難度。
ETag就是將原先協商緩存的比較時間戳的形式修改成了比較文件指紋。
文件指紋:根據文件內容計算出的唯一哈希值。文件內容一旦改變則指紋改變。
我們來看一下流程↓
1.第一次請求某資源的時候,服務端讀取文件並計算出文件指紋,將文件指紋放在響應頭的etag字段中跟資源一起返回給客戶端。
2.第二次請求某資源的時候,客戶端自動從緩存中讀取出上一次服務端返回的ETag也就是文件指紋。並賦給請求頭的if-None-Match字段,讓上一次的文件指紋跟隨請求一起回到服務端。
3.服務端拿到請求頭中的is-None-Match字段值(也就是上一次的文件指紋),並再次讀取目標資源並生成文件指紋,兩個指紋做對比。如果兩個文件指紋完全吻合,說明文件沒有被改變,則直接返回304狀態碼和一個空的響應體並return。如果兩個文件指紋不吻合,則說明文件被更改,那麼將新的文件指紋重新存儲到響應頭的ETag中並返回給客戶端
代碼圖例↓

流程示例圖↓

從校驗流程上來說,協商緩存的修改時間比對和文件指紋比對,幾乎是一樣的。
ETag也有缺點ETag需要計算文件指紋這樣意味着,服務端需要更多的計算開銷。。如果文件尺寸大,數量多,並且計算頻繁,那麼ETag的計算就會影響服務器的性能。顯然,ETag在這樣的場景下就不是很適合。
ETag有強驗證和弱驗證,所謂將強驗證,ETag生成的哈希碼深入到每個字節。哪怕文件中只有一個字節改變了,也會生成不同的哈希值,它可以保證文件內容絕對的不變。但是,強驗證非常消耗計算量。ETag還有一個弱驗證,弱驗證是提取文件的部分屬性來生成哈希值。因為不必精確到每個字節,所以他的整體速度會比強驗證快,但是準確率不高。會降低協商緩存的有效性。
值得注意的一點是,不同於cache-control是expires的完全替代方案(說人話:能用cache-control就不要用expiress)。ETag並不是last-modified的完全替代方案。而是last-modified的補充方案(說人話:項目中到底是用ETag還是last-modified完全取決於業務場景,這兩個沒有誰更好誰更壞)。
追加有掘友說👇
我來補足一下。
如何設置緩存從前端的角度來說:
你什麼都不用干,緩存是緩存在前端,但實際上代碼是後端的同學來寫的。如果你需要實現前端緩存的話啊,通知後端的同學加響應頭就好了。
從後端的角度來說
請參考文章,雖然文章里的後端是使用node.js寫的,但我寫了詳細的注釋。對於後端的同學來說。應該不難看懂。
哪些文件對應哪些緩存這個,我確實忘了說。哈哈哈。
有哈希值的文件設置強緩存即可。沒有哈希值的文件(比如index.html)設置協商緩存
為什麼有哈希值的文件設置強緩存


這是我打完包之後的css文件。大家是否注意到。我劃了紅線的部分。明顯,這絕不是我的文件名。這串和亂碼一樣的字符串叫哈希值。每次打包之後都會生產一串新的哈希值並追加到我們的文件名中。哈希值是打包後的文件名的一部分。
我們給css設置強緩存,哪怕緩存1W年。只要我們重新打包,生產新的哈希值。那麼文件名就更改了。對於機器來說,更改了文件名的文件,就是一個新的文件。
舉個例子👇
比如,有一個css文件a1
第一次打包a1.css文件追加哈希值變成了 a1.aaaaa.css,我們給a1.aaaaa.css設置了強緩存1W年。
然後項目改動,我們又打包了一次。打包後生產新的哈希值,a1.aaaaa.css變成了a1.bbbbb.css文件。那麼當我們第一次訪問a1.bbbbb.css文件的時候是不會被緩存。因為1W年的緩存是給a1.aaaaa.css文件做的。關我a1.bbbbb.css文件什麼事?這樣我們也就能拿到最新的改動。
其他可以被webpack生成哈希值的文件同理。
為什麼index.html使用協商緩存
既然img/css這些文件都可以用強緩存。通過更改文件名的方式來獲取最新的數據,為什麼我堂堂index.html就要用協商呢?
我給大家看個圖

因為一般情況下,index.html是不會設置哈希值的。(具體得看自己項目下的dist文件夾)
注意:哈希值是需要webpack生成的。不是天生的。不過有些框架會自帶(比如我使用的umi.js),設置緩存前務必看下自己的dist文件。因為如果沒有配置的話,你可能所有文件都不帶哈希值。
總結一下http緩存可以減少寬帶流量,加快響應速度。
關於強緩存,cache-control是Expires的完全替代方案,在可以使用cache-control的情況下不要使用expires
關於協商緩存,etag並不是last-modified的完全替代方案,而是補充方案,具體用哪一個,取決於業務場景。
有些緩存是從磁盤讀取,有些緩存是從內存讀取,有什麼區別?答:從內存讀取的緩存更快。
所有帶304的資源都是協商緩存,所有標註(從內存中讀取/從磁盤中讀取)的資源都是強緩存。
https://juejin.cn/post/7127194919235485733