Skip to content

更新、清理与回滚:避免半更新站点 ​

浏览器通过比较 worker 脚本字节来发现新版本。新 worker 会并行安装,但默认等待旧 worker 控制的页面全部离开;这正是避免“旧 HTML + 新 JS”半更新组合的保护。skipWaiting() 可以提前激活,clients.claim() 可以立即接管现有页面,却会让同一页面在运行中换控制者,必须配合兼容的资源版本和用户可见提示。

1. 版本化安装与激活 ​

js
const VERSION = 'shell-v2';
const CACHE_PREFIX = 'reader-';
const ASSETS = ['/', '/index.html', '/offline.html', '/app.v2.js'];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(`${CACHE_PREFIX}${VERSION}`).then(cache => cache.addAll(ASSETS))
  );
});

self.addEventListener('activate', event => {
  const keep = `${CACHE_PREFIX}${VERSION}`;
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key.startsWith(CACHE_PREFIX) && key !== keep)
        .map(key => caches.delete(key))
    ))
  );
});

新缓存先完整准备,旧缓存仍可服务现有页面;激活时只清理本应用前缀。若新资源下载失败,install 拒绝,旧 worker 和旧缓存继续工作,这就是天然的回滚路径。不要在 install 中删除旧缓存,否则失败时会留下无法启动的组合。

2. 什么时候允许立即更新 ​

内容阅读器可以在用户点击“立即更新”后发送消息,让 worker 执行 skipWaiting,然后页面监听 controllerchange 并整页重载:

js
// sw.js
self.addEventListener('message', event => {
  if (event.data?.type === 'ACTIVATE_NOW') self.skipWaiting();
});
js
// app.js
navigator.serviceWorker.addEventListener('controllerchange', () => {
  window.location.reload();
});

async function updateNow() {
  const registration = await navigator.serviceWorker.ready;
  registration.waiting?.postMessage({ type: 'ACTIVATE_NOW' });
}

这段代码不能解决数据库迁移、未保存表单或跨标签页状态;在这些场景应显示“新版本已准备好”,由用户确认后保存并刷新。clients.claim() 只适合你确认旧页面能接受新 worker 响应的场景,不是所有项目的默认模板。

3. 观测失败与回滚 ​

给注册状态打点:installing、waiting、active、redundant,并把 registration.update() 的异常记录到错误监控。发布时保留至少一个旧静态资源版本,避免 HTML 已经更新而 CDN/缓存中找不到旧 chunk。若新版本出现错误,服务端回滚入口应先恢复可加载的 worker 和资源,再考虑清理客户端缓存;客户端强删缓存不是万能回滚。

练习 ​

把本页 VERSION 改为 shell-v3,故意让 ASSETS 包含不存在的 /app.v3.js。验证新 worker 进入 redundant 或安装失败,而旧页面仍可刷新;修正文件后再次更新,并记录 DevTools Application 面板中的 waiting/active 转换。解释为什么把 skipWaiting() 写进每次 install 会隐藏这个安全检查。

下一步 ​

完成练习后查看本章答案,再学习安装、离线与同步。

用心记录每一步成长