...
Back

service worker 是一个你无法从服务端清掉的缓存

加上离线支持,等于在用户设备上放了一个可编程代理。它是你的部署里唯一无法回滚的部分 —— 所以更新路径比这个功能本身更重要。

service worker 是一个你无法从服务端清掉的缓存

service worker 是一个你无法从服务端清掉的缓存 📴

加 service worker 通常被当成一个小增强来介绍:离线兜底、二次访问更快、安装提示。从机制上说它确实小。从运维上说,它是你能放进 web 应用里后果最重的东西 —— 因为它是你的部署中唯一一个住在用户设备上、而你够不着的部分。

发出去一个坏的,每一个受影响的访客都会一直拿到那个坏版本。没有缓存清除、没有 CDN 失效、没有回滚。唯一的修法是发一个新的 service worker,并且要旧的那个允许它安装 —— 也就是说,更新机制必须在你发出去的其他一切都不工作时依然工作。


先设计更新路径,再设计离线行为

两条让这件事安全的规则。

绝不要长时间缓存 service worker 脚本本身。 它应该以很短或零的 max-age 提供。浏览器在这里有保护措施,但配置是你的 —— 而那一个文件上的长效缓存头,就是"一小时内修复上线"和"缓存过期时修复上线"之间的区别。

让新版本及时接管,并且是有意为之。 默认情况下,新 worker 会一直等到旧 worker 控制的每个标签页都关闭 —— 对一个被钉住的标签页来说,这可能是好几天。显式跳过等待并接管现有客户端,意味着修复在下一次加载时传播,而不是"迟早"。

这个选择有真实的代价:一个进行中的会话可能突然被送来另一个构建的资源。这正是第三条规则要紧的原因:

给缓存加版本,并在激活时清理。 在缓存名里带上构建标识,并在激活时删掉所有非当前的缓存。没有这一步,三次部署之前的陈旧条目会无限期存活,并产生这里最糟的一类 bug —— 一个由不匹配版本拼装起来的页面,HTML 期待的是一个资源哈希,而缓存给出的是另一个。


缓存什么,以及绝不缓存什么

安全的默认值是窄。缓存你自己的静态构建产物 —— 带哈希的资源、字体、一个离线页面。就这些,直到你有具体理由要更多。

在没有专门设计的情况下,绝不缓存:

  • 需要认证的响应。 在共享设备上把缓存的响应发给另一个用户,就是那个失败模式,而且它不是假想。
  • 任何带 Set-Cookie 或按会话变化的东西。
  • API 响应,除非你已经精确定义了那个端点的"陈旧"意味着什么、以及用户基于陈旧数据行动时会发生什么。
  • HTML 导航,除非你真的想要离线导航,并且想清楚了"提供一个其引用资源可能已不存在的页面"意味着什么。

对一个文档站或营销站来说,静态资源加一个离线兜底页,能拿到几乎全部收益,而几乎不承担风险。


离线页面是个真实的设计问题

大多数离线页面只说"你离线了",别的什么都不说 —— 而用户本来就知道。

更好的做法:说清楚还有什么可用。如果有缓存的文档页,把链接列出来。如果有正在编辑的草稿,确认它已经存在本地、将来会同步。如果什么都没有,就说连接恢复时页面会自动加载,并且真的自动重试,而不是要求用户手动刷新。

离线页面是唯一一个必定会在用户最糟糕的时刻被看到的界面。它值得比耸耸肩更多的东西。


测更新,不是测安装

安装一个 service worker 很容易验证。出问题的地方在更新,而这需要一次刻意的演练:

  1. 打开站点,确认 worker 已激活。
  2. 部署一个改动。
  3. 不清存储地重新加载 —— 用真实用户抵达的方式。
  4. 确认新版本已接管,且旧缓存已被删除。

然后演练那个紧急预案:发一个会注销自己并清空所有缓存的 worker,确认一台跑着上一版本的设备能恢复到干净状态。需要它之前就把这个逃生舱写好。在压力之下、为已经卡在坏版本上的用户临时造一个,不是你想身处的位置。


到底该不该加

诚实的判据是:离线使用对你的用户是不是一个真实场景。对一个在通勤路上阅读的文档站、或者一个在现场使用的工具,是的。对一个营销页面,那点性能收益很少能正当化"在每个访客设备上放一个不可回滚的组件"这件事。

功能很小。承诺不小 —— 而你实际在决定的,是那个承诺,不是那个功能。