解锁缓存新姿势——更灵活的 Cache

缓存大家族迎来了新的成员——Cache,可能考虑到 Application Cache、LocalStorage 这两个家伙的先天缺陷后天发育不良带来的问题,Google 和 Firefox 对其进行了基因重组,不过两边在如何重组的想法上有分歧,所以各自就分别各自造了去。

Chromium Cache API 设计者给 Cache API 的定位是“ServiceWorker 的一种新的应用缓存机制”,他们把 Cache API 定位为 Application Cache(虽然离线缓存的设计上存在重大缺陷,但是代码至少是无辜的),我们就很容易理解为什么 Chromium 内部的 Cache API 代码实现会大量复用 Application Cache 的代码,使用一样的存储类型(Temporary),使用一样的存储后端(Very Simple Backend)。

Firefox 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 实现一套新的存储机制。

你需要知道 CacheStorage

Cache 对象受到 CacheStorage 的管理,在 W3C 规范中,CacheStorage 对应到内核的 ServiceWorkerCacheStorage 对象。它提供了很多JS接口用于操作Cache 对象:

  • CacheStorage.open() 用于获取一个 Cache 对象实例。
  • CacheStorage.match() 用于检查 CacheStorage 中是否存在以Request 为 key 的 Cache 对象。
  • CacheStorage.has() 用于检查是否存在指定名称的 Cache 对象。
  • CacheStorage.keys() 用于返回 CacheStorage 中所有 Cache 对象的 cacheName 列表。
  • CacheStorage.delete() 用于删除指定 cacheName 的 Cache 对象。

在使用过程中,需要注意以下这些情况:

  • 任意 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 值。

前端从这些情况可以得到哪些信息呢?资源的存储不是按照资源的域名处理的,而是按照 Service Worker 的 origin 来处理,所以 Cache 的资源是无法跨域共享的,意思就是说,不同域的 Service Worker 无法共享使用对方的 Cache,即使是 Foreign Cache 请求的跨域资源,同样也是存放在这个 origin 之下。因为 ServiceWorkerCache 通过 cacheName 标记缓存版本,所以就会存在多个版本的 ServiceWorkerCache 资源。为什么需要 cacheName 来标记版本呢?

假设当前域名下所有的覆盖式发布的静态资源和接口数据全部存储在同一个 cacheName 里面,业务部署更新后,无法识别旧的冗余资源,单靠前端无法完全清除。这是因为 Service Worker 不知道完整的静态资源路径表,只能在客户端发起请求时去做判断,那些当前不会用到的资源不代表以后一定不会使用到。假如静态资源是非覆盖式发布,那么冗余的资源就更多了。这里要特别注意的是,Cache 不会过期,只能显式删除

如果版本更新后,更换 cacheName,这意味着旧 cacheName 的资源是不会使用到了,业务逻辑可以放心的把旧 cacheName 对应的所有资源全部清除,而无需知道完整的静态资源路径表。

那 cacheName 是不是只是在这种情况下才能发挥作用呢?其实不是的,使用过 webpack 工具的同学知道 vender 配置,vender 主要是把最不经常变动的第三方的库文件打包在一起,避免与频繁更新的资源打包一起,提高客户端缓存使用率,还有就是 common 的配置,把公用的组件打包在一起,减少代码冗余,因此,cacheName 也可以根据这种情况进行设置,最大化利用缓存空间,提高缓存利用率。

由于 Service Worker 相关缓存的底层存储都使用了系统的文件系统(File System),而文件系统一般是不支持多进程访问的,当统一域名下有两个不同的 Service Worker 是无法同时对同一资源进行操作的。

你更需要知道 Cache

规范里 Cache 对应内核的 ServiceWorkerCache 对象,提供了已缓存的 Request / Response 对象体的存储管理机制。它提供了一系列管理存储的JS接口:

  • 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 不会过期,只能显式删除 。

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 不同,是会分开存储的哦。

说到存储路径,那必然会涉及到存储的容量大小,Service Worker 规范并没有明确规定 ServiceWorkerCache 的容量限制,在 Chromium 50 以下版本的内核限制为 512M,Chromium 50 及以上版本内核不作限制(即为std::numeric_limits<int>::max)。当然,这只是 Service Worker 层面的限制,它还会受浏览器 QuotaManager 的限制。

QuotaManager 对每个域名可用存储空间也有限制,算法(Chromium57)可简单描述如下:

Temporary 类型存储限额 = 【系统磁盘可用空间(available_disk_space) + 浏览器全局已使用空间(global_limited_usage)】/ 3 (注:kTemporaryQuotaRatioToAvail = 3)

每个域名可使用 Temporary 类型存储限额 = Temporary 类型存储限额 / 5 (注:QuotaManager::kPerHostTemporaryPortion = 5)

比如,系统磁盘可用空间为 570M, 浏览器全局已使用空间为 30M,那么 每个域名可使用 Temporary 类型存储限额 = (570+30)/ 3 / 5 = 40M。虽然 ServiceWorkerCache 在 Service Worker 层面的限制为 512M,非常大,但它也不能超出每个域名的限制(40M),即同一域名下的 ServiceWorkerCache 也只能使用 40M。

一般来说,Service Worker 层面对 ServiceWorkerCache 的限制都会大于浏览器对每个域名的限制,所以,通常可理解为,ServiceWorkerCache 仅受浏览器 QuotaManager 对域名可使用存储的限制。对于前端开发同学来说,必须有清理冗余缓存的业务逻辑,并且提高缓存资源的使用率。

当 Service Worker 从 Cache 拿不到资源时,就会去 http cache 查找,找不到才去请求网络。

目前对 Cache API 的使用比较有限,后面有经验积累再继续补充。


转载于:https://juejin.im/post/595233e55188250d9208527c

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值