更新、清理与回滚:避免半更新站点
浏览器通过比较 worker 脚本字节来发现新版本。新 worker 会并行安装,但默认等待旧 worker 控制的页面全部离开;这正是避免“旧 HTML + 新 JS”半更新组合的保护。skipWaiting() 可以提前激活,clients.claim() 可以立即接管现有页面,却会让同一页面在运行中换控制者,必须配合兼容的资源版本和用户可见提示。
1. 版本化安装与激活
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 并整页重载:
// sw.js
self.addEventListener('message', event => {
if (event.data?.type === 'ACTIVATE_NOW') self.skipWaiting();
});// 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 会隐藏这个安全检查。