service worker 缓存原理

workbox 的 service worker 缓存实现

正如其名 workbox 是 google 封装的一个工具箱,旨在提供开箱即用的 service worker 相关特性。

precaching 缓存应该其中适用性最广的一个特性。正好最近在项目中使用的到,于是便去研究了一下其原理

service Worker + cache api = precache

我们说 service worker 缓存其实并不正确,service worker 本身并不提供缓存功能,而是配合了 Cache 的 api 才提供完整的缓存功能。

在 workbox 中将这两者封装成一个 workbox-precaching

Service Worker 生命周期

在缓存功能中 Service Worker 的作用主要有两个

  • 提供生命周期的钩子给 PrecacheController 进入执行缓存控制的相关逻辑
  • 提供拦截请求的功能

Service Worker 的生命周期包括

  • installing 每个 service worker 只触发一次,初始化和注册 Service Worker
  • installed Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。
  • activetaing 在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。
  • activate 在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件 fetch (请求)sync (后台同步)push (推送)
  • redundant 这个状态表示一个 Service Worker 的生命周期结束。

img

其中 precaching 用到的主要是 installingactivate 两个声明周期的钩子,分别对应了静态资源缓存的初始化,更新以及拦截请求

Cache API

precaching 的另一部分则是 Cache Api

Cache 的缓存机制可以直接对 Request / Response 对象对提供存储机制。有意思的是,虽然他定义在 service worker 的标准中,但是它直接暴露在 window 的作用域下,因此你实际上不一定需要配合 service worker 使用

cache 包括这样一些有趣的特性

  • 一个域可以有多个命名 Cache 对象
  • 你需要自定义更新方式,除非明确地更新缓存,否则缓存将不会被更新
  • 你需要自定义清理缓存逻辑,除非删除,否则缓存数据不会过期
  • Cache 的 Cache.put, Cache.addCache.addAll只能在GET请求下使用。

可以看到 Cache api 保存的直接就是一个 Request/ Response 的对象,因此可以直接其作为请求的返回。

另外,在写下这篇文章的节点, mdn 中 关于 Cache api 的部分文档和实际的 api 行为有不一致,我估计是没来得及更新。一个比较明显的差异在于 Cache.keys()

根据文档的说明和示例, Cache.keys() 返回应该是一个Promise对象,其 resolve 的结果是Cache对象key值组成的数组

如果用 ts 类型标注出来应该是 Promise<string[]|null> ,但是在 workbox 的源码中其标注的 types 是 Promise<Request[] | null>,两者显然不可相比。

额外的缓存清除机制

虽然说除非主动删除,否则缓存数据不会过期一直缓存下来,但有一种额外的的情况,那就是浏览器上的可控制存储大小实际上受限于硬盘的大小。

而浏览器会根据可控制存储大小,硬性的限制每个域下缓存数据的大小。所以 cache 的容量是有上限。如果某个域配额不够,或者磁盘空间不够的话,浏览还是会根据 LRU 最近最少使用的规则,清除某个域下面的缓存信息。清除的时候会以整个域作为单位进行清除,而不是再深入细分清除。

因此一个 workbox 完整的缓存控制流程如下

  • webpack 中 workbox 根据我们打包出来的 chunk 和相关的过滤配置,生成了 Manifest.js 记录所有要缓存的静态资源
    • manfest 的结构 {revision:string, url:string}
    • revision 保存文件的 md5 hash 作为文件的版本
    • url 则是 Cache 的 key 值和请求地址
  • installing 回调中读取 Manifest.js ,获取所需的静态资源索引,调用 cache api 进行缓存
  • activate 时根据 manifest ,对资源文件进行遍历检查,删除无用资源或者更新资源文件
  • fetch 事件中拦截 cache 中已经存在的静态资源文件请求直接返回缓存数据

对比其他缓存方式

http 缓存

http 主要依赖 304 进行缓存,包括

  • 强缓存:通过 Cache-Control max-age 设定一个过期时间,再这个时间之前都不会再次发请求
  • 协商缓存:强缓存失效之后,则可以通过 last-modified 或者 etag 向服务器请求判断是否需要更新缓存

http 的问题在于用户主动刷新的时候,就会让强缓存失效,直接进行协商缓存。

localstorage 缓存

localstorage 缓存相比 http 缓存可以进一步控制资源,也可以缓存比较大的资源,不过 localstorage 也有其问题

  • 需要自己实现资源的请求管理的机制,管理资源文件的读取和写入。
  • localstorage 只能保存字符串数据,因此也需要自己实现对资源的序列化,反序列化,以及添加的逻辑
  • seo 不友好
  • 版本更新机制

所以相比之下 Cache + Service Worker 的优势就是对 localstorage 缓存方案的进一步改进

  • 不需要自己去维护资源的请求机制
  • service Worker 可以直接拦截请求,cache 直接保存 Request / Response不需要进行序列化
  • seo 优化
  • 可控性更加的高