前端專案修改了很多東西:比如bug啊,樣式啊。當你把前端專案打包之後滿心歡喜的在 Nginx(測試環境)換上它,然後在 Jira 上修改bug狀態@測試人員複測。然後測試人員開始找你battle了,你的bug怎麼還是沒修改啊,但是你明明換上了最新的版本,中間到底出現了什麼問題。開啟控制檯的 network,顯示如圖所示。
問題就出在 from disk cache 這玩意上,也就是瀏覽器快取,瀏覽器讀取的還是快取中舊版的資源,渲染出來的還是舊版的效果。除了 disk cache 外,還有其他幾類瀏覽器快取,總的來說,瀏覽器快取大致分為4種,而這4種方式是有優先順序順序的,只有依次查詢快取且都沒有命中的時候,才會去請求網路:
-
Service Worker:是一種獨立於主執行緒之外的 Javascript 執行緒。它脫離於瀏覽器窗體,可以幫我們實現離線快取、訊息推送和網路代理等功能。
-
Memory Cache:存在記憶體中的快取。包括當前中頁面中已經抓取到的資源,例如頁面上已經下載的樣式、指令碼、圖片等。因為儲存在記憶體中,MemoryCache 是響應速度最快的一種快取,但由於同樣的原因,快取持續性很短,會隨著程式的釋放而釋放,一旦我們關閉 Tab 頁面,記憶體中的快取也就被釋放了。
-
Disk Cache:Disk Cache 也就是儲存在硬碟中的快取,讀取速度慢點,但是什麼都能儲存到磁碟中,比之 Memory Cache 勝在容量和儲存時效性上。會根據 HTTP Herder 中的欄位判斷哪些資源需要快取,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。
-
Push Cache:Push Cache 是 HTTP2 在 server push 階段存在的快取,當以上三種快取都沒有命中時,它才會被使用,Push Cache 是一種存在於會話階段的快取,當 session 終止時,快取也隨之釋放。不同的頁面只要共享了同一個 HTTP2 連線,那麼它們就可以共享同一個 Push Cache。
-
其實常見的情況下只有 disk cache 和 memory cache,如下圖部落格園首頁請求所示:
至於什麼情況下是存在記憶體,還是存在硬碟。由於計算機記憶體有限,而且比硬碟容量小很多,瀏覽器會根據計算機具體情況來決定快取放在記憶體中還是硬碟中。一般情況下,較大的 css 檔案、js 檔案和 jpg 圖片這類大檔案會被存入硬碟;當前系統記憶體使用率高的話,檔案也會被優先儲存進硬碟;而 Base64 格式的圖片,幾乎永遠可以被塞進記憶體。
那為什麼需要快取呢,對前端來說,因為讀快取不需要發起請求,也就不需要考慮請求環境和速度,提高訪問速度,使用者體驗大大提升;對於後端而言,也緩解了伺服器的壓力,減少網路 IO 消耗,減少頻寬消耗。
但是什麼時候需要快取,什麼時候不需要快取。很明顯,我這種換版操作肯定是需要重新獲取新的資源的。最簡單的解決辦法就是 Ctrl+F5 強制重新整理,強制告訴瀏覽器不獲取快取,必須重新去獲取新的資源,但是強制重新整理這種手動觸發還是對使用者體驗不太友好。特別是我們做的後臺管理系統,在圖片很少的情況下,有沒有辦法每次換版之後都獲取最新的資源。這個時候就要涉及瀏覽器的快取策略了,常見的快取策略有強快取和協商快取。其實瀏覽器的快取策略都是透過設定 HTTP Header 來實現的。
強快取
不會向伺服器傳送請求,直接從快取中讀取資源。狀態碼:200,顯示 from disk cache 或 from memory cache。透過設定兩種 HTTP Header 實現:Expires和Cache-Control。
1.Expires:值是一個時間戳,表示本地時間到這個設定的時間快取就失效。這樣一來 Expires 就是有問題的,受限於本地時間,我直接手動去把電腦端的時間改掉,都能導致快取失效,所以更推薦使用 Cache-Control,或者二者搭配使用。
在 Nginx中配置寫法如下:
# gif、jpg、jpeg、png、bmp、ico這類的資源會在30天后失效 location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ { root /XXXX/xxxx; expires 30d; }
2.Cache-Control:優先順序比 Expires 高,同時設定 Expires 和 Cache-Control 則後者生效。可以在請求頭或者響應頭中設定,並且可以組合使用多種指令:
- private(預設):只能在瀏覽器中快取, 只有在第一次請求的時候才訪問伺服器,若有 max-age,則快取期間不訪問伺服器
- public:可以被任何快取區快取,如:瀏覽器、伺服器、代理伺服器等
- no-cache:可以快取,但每次都應該去伺服器驗證快取是否可用,進入協商快取階段,若有 max-age,則快取期間不訪問伺服器,
- no-store:不僅不能快取,連暫存也不可以(即:臨時資料夾中不能暫存該資源)
- max-age=<seconds>:以秒為單位的快取時間,max-age=60,表示快取60秒後失效,60秒內再次訪問該資源,均使用本地的快取,不再向伺服器發起請求
- s-maxage=<seconds>:同 max-age 作用一樣,只在代理伺服器中生效(比如CDN快取),s-maxage 優先順序高於 max-age,只對 public 快取有效。設定了 s-maxage,沒設定 public,代理伺服器也可以快取這個資源
- must-revalidate:可快取但必須再向源伺服器進確認
- proxy-revalidate:要求中間快取伺服器對快取的響應有效性再進行確認
在 Nginx 中配置寫法如下,隨便舉一個指令:
location ~ .*\.(css|js|swf|php|htm|html )$ { add_header Cache-Control no-store; }
協商快取
當 Cache-Control 和 Expires 過期或者它的屬性設定為 no-cache 時(即不走強快取),那麼瀏覽器第二次請求時就會與伺服器進行協商,伺服器端會對比判斷資源是否進行了修改更新,對比結果無非是以下兩種:
-
- 如果伺服器端的資源沒有修改(Not Modified),那麼就會返回304狀態碼,告訴瀏覽器可以使用快取中的資料。
- 如果資料有更新就會返回200狀態碼,伺服器就會返回更新後的資源並且將快取資訊一起返回。
至於瀏覽器是怎麼和伺服器互動,主要是依靠跟協商快取相關的header頭屬性:Last-Modified/If-Modified-Since、ETag/If-None-Match。這些屬性在請求頭和響應頭是成對出現的。
1.Last-Modified/If-Modified-Since:
瀏覽器在第一次訪問資源,或快取過期後訪問,伺服器返回資源的同時,在 response header 中新增 Last-Modified 的 header,值是這個資源在伺服器上的最後修改時間,瀏覽器接收快取檔案和header資訊。隨後我們每次請求時,瀏覽器會自動帶上一個叫If-Modified-Since 的時間戳欄位給伺服器,它的值正是上一次 response 返回給它的 Last-modified 值,然後伺服器會根據 If-Modified-Since 的值對比資源的最後修改時間判斷資源是否進行了修改更新。
2.ETag/If-None-Match :
Etag是由伺服器為每個資源生成的唯一的標識字串,這個標識字串是基於檔案內容編碼的,只要檔案內容不同,它們對應的 Etag 就是不同的,因此 Etag 能夠精準地感知檔案的變化。Etag 和 Last-Modified 類似,當首次請求時,我們會在響應頭裡獲取到一個最初的識別符號字串。那麼下一次請求時,瀏覽器就會在請求頭裡就會帶上一個值相同的、名為 if-None-Match 的字串供伺服器比對。Etag 的優先順序會比 Last-Modified 高,但是Etag因為要生成,也會更消耗伺服器效能。
檢視 Nginx 更新日誌可知,在2014年6月26日就預設開啟 Etag,對應的版本為1.7.3,也就是說1.7.3及以上的版本的 Nginx 預設開啟 Etag。不過需要注意的是,如果 Nginx 有開啟 Gzip,可能會與 Etag 有衝突。
然後就是各家的 Etag 生成情況都不太一樣,取決於伺服器的型別或配置的演算法。以下是簡書首頁隨便的一個請求,這個不是什麼大問題,順便提一嘴。
說了這麼多,前端快取最理想的效果就是希望能儘可能多的命中強快取,對於頻繁變動的資源,多使用協商快取,同時,能在更新版本的時候讓瀏覽器的快取失效。這就要求了我們對資源進行 Nginx 配置的時候,對資源失效時間有個自己的衡量和把握。
最後,還有一種情況是瀏覽器在幾次重新整理過程中會出現新版效果,也會出現舊版效果,新舊交替。那就得考慮前端專案是否佈置了多節點,並使用 Nginx 配置負載均衡了,如果是這個問題的話,只要全部 Nginx 節點環境都換上新打的前端包問題就迎刃而解了。