如何熱更新長緩存的 HTTP 資源

前言

HTTP 緩存時間一直讓開發者頭疼。時間太短,性能不夠好;時間太長,更新不及時。當遇到嚴重問題需緊急修復時,儘管後端文件可快速替換,但前端文件仍從本地緩存加載,導致更新長時間無法生效。

對於這個問題,很多網站都有相應的解決方案。

傳統方案

最常見的方案,就是爲靜態資源設置很長的緩存時間,然後在文件名中加入版本號或 Hash 值等字符,這樣文件更新後 URL 會變化,即可避開之前版本的緩存。

不過這種方案也有缺陷。假設網頁中有如下依賴關係的文件:

HTML
└─JS
   └─CSS
      └─GIF

現在需緊急更新 GIF 文件,於是重新發布 GIF 得到新的 URL。然而 CSS 中引用的仍是之前的 GIF URL,因此 CSS 也要修改和發佈,從而得到新的 CSS URL,進而導致加載該 CSS 的 JS 也得更新,加載該 JS 的 HTML 也得更新。

由此可見,快速更新一個文件需更新整個依賴鏈,白白浪費不少流量。同時產生大量無意義的新版本文件,它們的業務功能並無變化,只是爲了修改其中的 URL 而已。

如果能通過 JS 刪除某個文件的 HTTP 緩存,這樣就不用更換 URL 了,更不必修改其他文件。是否有這樣的黑科技?

高級方案

現代瀏覽器添加了很多新特性,其中有些和緩存相關。

例如 Clear-Site-Data 特性。當 HTTP 存在 Clear-Site-Data: "cache" 響應頭時,即可刪除該站點的緩存文件。不過它會刪除所有文件,而無法指定文件,因此不適合本案例。

例如 Fetch API 中 Request 對象的 cache 屬性也涉及緩存操作。閱讀文檔,其中 no-cache 非常有趣:

no-cache — The browser looks for a matching request in its HTTP cache.

  • If there is a match, fresh or stale, the browser will make a conditional request to the remote server. If the server indicates that the resource has not changed, it will be returned from the cache. Otherwise the resource will be downloaded from the server and the cache will be updated.
  • If there is no match, the browser will make a normal request, and will update the cache with the downloaded resource.

我們先看第一段:如果請求的文件存在緩存,無論是否過期,瀏覽器都會和後端協商緩存。未變化則使用本地緩存(HTTP 304),有變化則重新下載並 更新緩存

雖然無法刪除 HTTP 緩存,但能更新緩存內容,也是不錯的。

演示

基於該特性,這裏做了個演示:https://www.etherdream.com/cache-purge/

該頁面引用了 res.js ,其內容每隔 5s 遞增,可通過頁面中 Res Ver 顯示。

由於 res.js 設置了很長的緩存時間,因此不斷刷新頁面 Res Ver 也不會變化。(Chrome 刷新時只有頁面走協商緩存,內部資源仍從本地緩存加載)

點擊 Purge 按鈕,再次刷新頁面,這時 Res Ver 變化了。之後不斷刷新,數字仍保持不變。

通過控制檯可見,no-cache 請求不會直接使用本地緩存,並且返回的內容會覆蓋本地緩存。從而實現 HTTP 長緩存資源也能熱更新

如果 5s 內再次點擊 purge,文件不會重新下載,而是協商返回 304 狀態,符合文檔中的描述。

應用

這個特性如何應用到實際業務中?首先看下兼容性:

目前絕大多數瀏覽器都支持。如果不考慮低版本瀏覽器,那該如何使用?

顯然需要一個清單列表,記錄哪些文件需熱更新。同時爲了防止重複更新,還需記錄每個文件的版本號:

/foo.gif   100
/bar.gif   101
...

熱更新後將 URL 和版本號記錄到本地存儲中。之後再次執行時,如果版本號和本地存儲中的相同,就不必再更新了。

至於什麼時候執行,最簡單的當然是頁面打開時執行,但這不是最好的,因爲:

  1. 每次打開頁面都要加載清單文件,增加請求

  2. 熱更新需要一定的時間(包括加載清單),頁面初始化時不會等你,它仍使用現有的緩存資源

頁面運行時定期執行,可解決第 2 個問題,從而在下次訪問頁面之前完成更新,但輪詢清單會帶來更多請求。

如需減少請求,可使用服務端推送技術,當清單變化時主動推送給前端。這樣不僅可減少請求,而且前端能在第一時間更新。

缺陷

不過這個方案仍有缺陷。我們的初衷是刪除緩存,等業務再次使用時才下載;而本方案卻是更新緩存,提前將文件下載到本地,附帶預加載的功能。

如果熱更新的是常用的公共文件倒還好,但如果只是小部分用戶纔會用到的文件,卻讓所有用戶都提前預加載,這顯然不合理,反而更浪費流量。

終極方案

即使不能操作 HTTP 緩存,但如果能攔截 HTTP 請求並返回自定義內容,同樣可達到熱更新的效果。這正是 Service Worker API 的設計初衷。

開發者只需提供一個清單,將原始 URL 對應到最新 URL:

/foo.gif
  https://cdn.mysite.com/1.0.0/foo.gif

/bar.gif
  https://cdn.mysite.com/1.0.1/bar.gif

Service Worker 根據清單中的地址反向代理。這樣,只需更新一個清單文件,即可更新所有 URL 的內容。

得益於 Service Worker 能在瀏覽器後臺持續運行,因此其創建的 WebSocket 可長時間保持連接狀態,結合後端的更新推送服務,可大幅減少加載清單的開銷。

基於這個功能實現的演示:https://github.com/EtherDream/freecdn/tree/master/examples/quick-update

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