詳解前端緩存,解決前端換包之後環境中仍會出現舊版效果

  前端項目修改了很多東西:比如bug啊,樣式啊。當你把前端項目打包之後滿心歡喜的在 Nginx(測試環境)換上它,然後在 Jira 上修改bug狀態@測試人員複測。然後測試人員開始找你battle了,你的bug怎麼還是沒修改啊,但是你明明換上了最新的版本,中間到底出現了什麼問題。打開控制檯的 network,顯示如圖所示。

  問題就出在 from disk cache 這玩意上,也就是瀏覽器緩存,瀏覽器讀取的還是緩存中舊版的資源,渲染出來的還是舊版的效果。除了 disk cache 外,還有其他幾類瀏覽器緩存,總的來說,瀏覽器緩存大致分爲4種,而這4種方式是有優先級順序的,只有依次查找緩存且都沒有命中的時候,纔會去請求網絡:

    1. Service Worker:是一種獨立於主線程之外的 Javascript 線程。它脫離於瀏覽器窗體,可以幫我們實現離線緩存、消息推送和網絡代理等功能。

    2. Memory Cache:存在內存中的緩存。包括當前中頁面中已經抓取到的資源,例如頁面上已經下載的樣式、腳本、圖片等。因爲存儲在內存中,MemoryCache 是響應速度最快的一種緩存,但由於同樣的原因,緩存持續性很短,會隨着進程的釋放而釋放,一旦我們關閉 Tab 頁面,內存中的緩存也就被釋放了。

    3. Disk Cache:Disk Cache 也就是存儲在硬盤中的緩存,讀取速度慢點,但是什麼都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上。會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。

    4. 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 則後者生效。可以在請求頭或者響應頭中設置,並且可以組合使用多種指令:

    1. private(默認):只能在瀏覽器中緩存, 只有在第一次請求的時候才訪問服務器,若有 max-age,則緩存期間不訪問服務器
    2. public:可以被任何緩存區緩存,如:瀏覽器、服務器、代理服務器等
    3. no-cache:可以緩存,但每次都應該去服務器驗證緩存是否可用,進入協商緩存階段,若有 max-age,則緩存期間不訪問服務器,
    4. no-store:不僅不能緩存,連暫存也不可以(即:臨時文件夾中不能暫存該資源)
    5. max-age=<seconds>:以秒爲單位的緩存時間,max-age=60,表示緩存60秒後失效,60秒內再次訪問該資源,均使用本地的緩存,不再向服務器發起請求
    6. s-maxage=<seconds>:同 max-age 作用一樣,只在代理服務器中生效(比如CDN緩存),s-maxage 優先級高於 max-age,只對 public 緩存有效。設置了 s-maxage,沒設置 public,代理服務器也可以緩存這個資源
    7. must-revalidate:可緩存但必須再向源服務器進確認
    8. 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 節點環境都換上新打的前端包問題就迎刃而解了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章