解鎖緩存新姿勢——更靈活的 Cache
緩存大家族迎來了新的成員——Cache,可能考慮到 Application Cache、LocalStorage 這兩個傢伙的先天缺陷後天發育不良帶來的問題,Google 和 Firefox 對其進行了基因重組,不過兩邊在如何重組的想法上有分歧,所以各自就分別各自造了去。
nChromium Cache API 設計者給 Cache API 的定位是「ServiceWorker 的一種新的應用緩存機制」,他們把 Cache API 定位為 Application Cache(雖然離線緩存的設計上存在重大缺陷,但是代碼至少是無辜的),我們就很容易理解為什麼 Chromium 內部的 Cache API 代碼實現會大量復用 Application Cache 的代碼,使用一樣的存儲類型(Temporary),使用一樣的存儲後端(Very Simple Backend)。
nFirefox Cache API 設計者 在博客文章中描述了他的想法,最初是想重用 HTTP Cache 或者 基於 IndexedDB 去實現,但 Cache API 規範在不斷演進,一些規範細節與上述解決方案存在不可調和的衝突。比如,HTTP Cache 中,一個 URL 只能對應一個 Response,但 Cache API 規範要求同一 URL(不同的 Header)可以對應多個 Response,另外,HTTP Cache 沒有使用容量管理系統(QuotaManager)而 Cache API 需要使用。IndexedDB 基於結構克隆(structured cloning),還不支持流式數據(streaming data),這樣,Response可能會非常大,從網路回來會非常慢,會明顯增大內存使用。基於上述原因,Firefox 決定基於 SQLite 為 Cache API 實現一套新的存儲機制。
n
你需要知道 CacheStorage
nCache 對象受到 CacheStorage 的管理,在 W3C 規範中,CacheStorage 對應到內核的 ServiceWorkerCacheStorage 對象。它提供了很多JS介面用於操作Cache 對象:
n- CacheStorage.open() 用於獲取一個 Cache 對象實例。
- CacheStorage.match() 用於檢查 CacheStorage 中是否存在以Request 為 key 的 Cache 對象。
- CacheStorage.has() 用於檢查是否存在指定名稱的 Cache 對象。
- CacheStorage.keys() 用於返回 CacheStorage 中所有 Cache 對象的 cacheName 列表。
- CacheStorage.delete() 用於刪除指定 cacheName 的 Cache 對象。
n
n
n
n
n
在使用過程中,需要注意以下這些情況:
n- 任意 CacheStorage 方法的調用,都有機會引起創建 ServiceWorkerCacheStorage 對象。
- ServiceWorkerCacheStorageManager 維護一個 cache_storage_map_(std::map<GURL, ServiceWorkerCacheStorage*>),這個 map 管理了所有的 origin + ServiceWorkerCacheStorage。
- 任何一個域名(比如,origin: https://chaoshi.m.tmall.com/)只會創建一個 ServiceWorkerCacheStorage 對象。
- ServiceWorkerCacheStorage 維護一個 cache_map_(std::map<std::string, base::WeakPtr<ServiceWorkerCache> >),這個 map 管理了同一 origin 下所有的 cacheName + ServiceWorkerCache。
- 同一域名下的 ServiceWorkerCacheStorage 都放在同一目錄,目錄路徑 storage_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003。其中8f9fa7c394456a3f75c7c0aca39d897179ba4003 是 origin(https://chaoshi.m.tmall.com/)的 hash 值。
n
n
n
n
n
前端從這些情況可以得到哪些信息呢?資源的存儲不是按照資源的域名處理的,而是按照 Service Worker 的 origin 來處理,所以 Cache 的資源是無法跨域共享的,意思就是說,不同域的 Service Worker 無法共享使用對方的 Cache,即使是 Foreign Cache 請求的跨域資源,同樣也是存放在這個 origin 之下。因為 ServiceWorkerCache 通過 cacheName 標記緩存版本,所以就會存在多個版本的 ServiceWorkerCache 資源。為什麼需要 cacheName 來標記版本呢?
n
假設當前域名下所有的覆蓋式發布的靜態資源和介面數據全部存儲在同一個 cacheName 裡面,業務部署更新後,無法識別舊的冗餘資源,單靠前端無法完全清除。這是因為 Service Worker 不知道完整的靜態資源路徑表,只能在客戶端發起請求時去做判斷,那些當前不會用到的資源不代表以後一定不會使用到。假如靜態資源是非覆蓋式發布,那麼冗餘的資源就更多了。這裡要特別注意的是,Cache 不會過期,只能顯式刪除
n如果版本更新後,更換 cacheName,這意味著舊 cacheName 的資源是不會使用到了,業務邏輯可以放心的把舊 cacheName 對應的所有資源全部清除,而無需知道完整的靜態資源路徑表。
n那 cacheName 是不是只是在這種情況下才能發揮作用呢?其實不是的,使用過 webpack 工具的同學知道 vender 配置,vender 主要是把最不經常變動的第三方的庫文件打包在一起,避免與頻繁更新的資源打包一起,提高客戶端緩存使用率,還有就是 common 的配置,把公用的組件打包在一起,減少代碼冗餘,因此,cacheName 也可以根據這種情況進行設置,最大化利用緩存空間,提高緩存利用率。
n由於 Service Worker 相關緩存的底層存儲都使用了系統的文件系統(File System),而文件系統一般是不支持多進程訪問的,當統一域名下有兩個不同的 Service Worker 是無法同時對同一資源進行操作的。
n你更需要知道 Cache
n
規範里 Cache 對應內核的 ServiceWorkerCache 對象,提供了已緩存的 Request / Response 對象體的存儲管理機制。它提供了一系列管理存儲的JS介面:
n- Cache.put() 用於把 Request / Response 對象體放進指定的 Cache。
- Cache.add() 用於獲取一個 Request 的 Response,並將 Request / Response 對象體放進指定的 Cache。註:等價於 fetch(request) + Cache.put(request, response)。
- Cache.addAll() 用於獲取一組 Request 的 Response,並將該組 Request / Response 對象體放進指定的Cache。
- Cache.keys() 用於獲取 Cache 中所有 key。
- Cache.match() 用於查找是否存在以 Request 為 key 的Cache 對象。
- Cache.matchAll() 用於查找是否存在一組以 Request 為 key 的 Cache 對象組。
- Cache.delete() 用於刪除以 Request 為 key 的 Cache Entry。注意,Cache 不會過期,只能顯式刪除 。
n
n
n
n
n
n
n
ServiceWorkerCache 對應的存儲目錄是 /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003/7353b21ee437f3877043ae17a5d5ba6395fdbd31,其中 7353b21ee437f3877043ae17a5d5ba6395fdbd31 是 cacheName(tm/chaoshi-fresh/4.2.17)的 hash 值(使用 base::SHA1HashString 計算)。相同的資源名稱,如果 cacheName 不同,是會分開存儲的哦。
n說到存儲路徑,那必然會涉及到存儲的容量大小,Service Worker 規範並沒有明確規定 ServiceWorkerCache 的容量限制,在 Chromium 50 以下版本的內核限制為 512M,Chromium 50 及以上版本內核不作限制(即為std::numeric_limits<int>::max)。當然,這只是 Service Worker 層面的限制,它還會受瀏覽器 QuotaManager 的限制。
nQuotaManager 對每個域名可用存儲空間也有限制,演算法(Chromium57)可簡單描述如下:
nTemporary 類型存儲限額 = 【系統磁碟可用空間(available_disk_space) + 瀏覽器全局已使用空間(global_limited_usage)】/ 3 (註:kTemporaryQuotaRatioToAvail = 3)
n
每個域名可使用 Temporary 類型存儲限額 = Temporary 類型存儲限額 / 5 (註:QuotaManager::kPerHostTemporaryPortion = 5)
n比如,系統磁碟可用空間為 570M, 瀏覽器全局已使用空間為 30M,那麼 每個域名可使用 Temporary 類型存儲限額 = (570+30)/ 3 / 5 = 40M。雖然 ServiceWorkerCache 在 Service Worker 層面的限制為 512M,非常大,但它也不能超出每個域名的限制(40M),即同一域名下的 ServiceWorkerCache 也只能使用 40M。
n一般來說,Service Worker 層面對 ServiceWorkerCache 的限制都會大於瀏覽器對每個域名的限制,所以,通常可理解為,ServiceWorkerCache 僅受瀏覽器 QuotaManager 對域名可使用存儲的限制。對於前端開發同學來說,必須有清理冗餘緩存的業務邏輯,並且提高緩存資源的使用率。
n當 Service Worker 從 Cache 拿不到資源時,就會去 http cache 查找,找不到才去請求網路。
n
目前對 Cache API 的使用比較有限,後面有經驗積累再繼續補充。
推薦閱讀:
※首屆螞蟻金服體驗科技大會
※「小白DAY7」細談設計稿還原
※堅定地使用 CSS Custom Properties
※ELSE 技術周刊(2017.11.20期)
※[譯文] 8pt柵格系統 - 2. 如何使用