Service Worker 的 skipWaiting 需要更新纪律,而非立即重载
强制激活新 service worker 可能导致数据丢失。安全做法如下。
为什么 skipWaiting() 对 PWA 更新很重要
Service worker 有一个故意延迟新版本接管的生命周期:安装中的 worker 在完成安装后才进入 waiting 状态,用户必须关闭所有标签页或手动触发激活才能使其变为 active。
这种设计防止会话中出现突兀的中断,但也拖慢了紧急或低风险更新的速度。skipWaiting() 方法通过强制等待中的 worker 立即激活,填补这一空缺。
MDN 的 service worker 生命周期文档指出,skipWaiting() 不是自动的——它需要明确调用,通常与用户提示搭配。
Service Worker 如何工作
三种状态
- Installing:Worker 正在下载与解析资源。脚本在此处调用
self.skipWaiting()。 - Waiting:Worker 已成功安装,但在所有标签关闭之前等待激活。
- Active:Worker 正在控制页面并处理 fetch 事件。
自动与手动激活
- 手动默认:用户必须关闭你 origin 的所有打开标签页, worker 才会激活。
- skipWaiting():即使标签打开,立即强制激活。
正确实现 skipWaiting()
步骤 1:在 Worker 中调用 skipWaiting()
// sw.js
self.addEventListener('install', (event) => {
// 预缓存关键资源……
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/offline.html',
'/styles.css',
]);
})
);
});
// 激活行为
self.addEventListener('activate', (event) => {
// 清理旧缓存
event.waitUntil(
caches.keys().then((cacheKeys) => {
return Promise.all(
cacheKeys
.filter((key) => key.startsWith('v') && key !== 'v1')
.map((key) => caches.delete(key))
);
})
);
});
// 如果你想立即激活(用户控制):
self.addEventListener('install', (event) => {
const skipWaitingPromise = self.skipWaiting();
event.waitUntil(skipWaitingPromise);
});关键:始终在事件监听器中(通常是 install)调用 skipWaiting(),并用 event.waitUntil() 包裹。不要在 fetch 处理或其他事件中随意调用。
步骤 2:在客户端通知用户
if ('serviceWorker' in navigator) {
let registration;
navigator.serviceWorker.register('/sw.js').then((reg) => {
registration = reg;
// 监听新 worker 进入 waiting 状态
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本等待中
showUpdateAvailable();
}
});
});
});
}
function showUpdateAvailable() {
const updateButton = document.createElement('button');
updateButton.textContent = 'Update now';
updateButton.onclick = () => {
if (registration && registration.waiting) {
// 向等待中的 worker 发送消息以 skip waiting
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
};
// 显示非侵入式提示/横幅
document.body.appendChild(createUpdateUI(updateButton));
}
// 监听 controller change(worker 激活)
navigator.serviceWorker.addEventListener('controllerchange', () => {
// 理想情况下,提供"刷新"选项或在用户确认后自动刷新
window.location.reload();
});步骤 3:在 Worker 中处理 skipWaiting 消息
// sw.js
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});为什么用消息? 直接从客户端调用 skipWaiting() 不可能——必须向 worker 发送消息。
常见陷阱
1. 不通知用户就自动调用 skipWaiting()
每次更新都调用 skipWaiting() 可能:
- 打断会话中的表单提交或事务
- 失效客户端状态(IndexedDB、LocalStorage 假设)
- 导致布局偏移或竞态条件错误
指导:仅对低风险更新(小 CSS 调整、HTML 模板更改)跳过等待,或经明确用户同意。
2. 未清理旧缓存
新 worker 激活时,你必须清除之前版本的缓存项,避免存储膨胀与服务job资源:
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== currentCacheName) {
return caches.delete(cacheName);
}
})
);
})
);
});3. 激活期间竞态条件
如果客户端在 controllerchange 后不立即刷新,它们可能受新 worker 控制,但仍持有旧假设:
- 缓解:最安全的方式是在 controllerchange 后立即刷新或提示“刷新”。
4. 忽略 naviationPreload
对于高度依赖网络响应的 PWA,skipWaiting() 未与 navigation preload 协调可能导致重复请求或处理失败:
// sw.js
navigationPreload.enable('/lookup?src=' + self.registration.scope);在客户端用 navigator.serviceWorker.ready 测试可降低竞态风险。
类型化的更新策略
| 更新类型 | 使用 skipWaiting()? | 理由 | |---------|-------------------|------| | 核心bug修复 | ✅ | 立即提升用户体验 | | 低风险 CSS/HTML 调整 | ✅ | 破坏风险极小 | | JavaScript 逻辑变更 | ⚠️ | 可能中断进行中的操作 | | 资源包变更(JS/CSS) | ✅ | 用户立即受益;通过缓存名协调 | | IndexedDB schema 变更 | ⚠️ | 确保迁移路径 | | 离线策略转变 | ⚠️ | 跳过前充分测试 |
用户体验模式
模式 1:"Update Available" 提示
function createUpdateToast() {
const toast = document.createElement('div');
toast.className = 'update-toast';
toast.innerHTML = `
<span>新版本可用。</span>
<button id="updateNow">Refresh</button>
`;
document.body.appendChild(toast);
toast.querySelector('#updateNow').onclick = () => {
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
};
}
// 每会话仅提示一次
const updateShown = sessionStorage.getItem('updateShown');
if (!updateShown) {
sessionStorage.setItem('updateShown', 'true');
createUpdateToast();
}模式 2:应用内更新横幅
内容型 PWA:
- 在顶部/底部添加小横幅
- 包含“Dismiss”与“Update”按钮
- 通过 LocalStorage 持久化是否忽略
模式 3:低风险更新静默刷新
非事务型应用:
if (registration.waiting && currentVersion === 'newStableVersion') {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
// controllerchange 后允许客户端自动刷新
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
}何时避免:电商、银行、重表单应用。
验证清单
部署自动 skipWaiting() 前:
Worker 侧
- [ ]
skipWaiting()在事件监听器内调用,并用event.waitUntil()包裹 - [ ] 消息处理器通过
postMessage()监听SKIP_WAITING - [ ]
activate事件清理旧缓存 - [ ] 缓存名已版本化(
v1、v2)以区分资源
客户端侧
- [ ]
updatefound监听器检测新 worker - [ ] 通过非侵入式 UI(提示、横幅)通知用户
- [ ] 用户接受更新时通过
postMessage向 waiting worker 发送消息 - [ ]
controllerchange监听器触发页面刷新或手动通知用户
测试
- [ ] 打开多标签测试:验证
controllerchange无需关闭标签即可触发 - [ ] 测试缓存清理:确认旧版本不累积
- [ ] 测试会话中表单提交:确保更新中不丢失数据
- [ ] 测试离线模式:验证新版本正确缓存并可离线工作
不同应用类型
电商
- ❌ 避免自动 skipWaiting。
- ✅ 用用户可控刷新,并保留购物车状态。
- ✅ 协调 indexedDB 迁移。
内容/资讯
- ✅ 激进更新提升及时性。
- ✅ 对低风险更改使用静默刷新。
- ⚠️ 注意阅读状态(文章滚动位置)。
生产力/工具
- ⚠️ 更新常影响逻辑;要求用户确认。
- ✅ 对 reload 保留文档状态(例如草稿内容)。
游戏
- ⚠️ 会话中更新有害;要求会话结束时刷新。
- ✅ 使用“更新就绪,继续当前局”消息。
浏览器差异
- Chrome:
skipWaiting()广泛支持;在 DevTools Application 标签检查行为。 - Edge:基于Chromium,与 Chrome 一致。
- Firefox:支持
skipWaiting(),但存在一些 devcache 细微差异。 - Safari:Service worker 可用,但激活生命周期略有不同;充分测试。
工具与调试
DevTools Service Worker 面板
- 检查
installing、waiting和activeworker。 - 通过 UI 手动触发
skipWaiting进行测试。 - 通过
console.log()查看来自 worker 的日志。
Workbox 指导
若使用 Workbox,其 skipWaiting() 设置简化生命周期处理:
workbox.core.clientsClaim();
workbox.precaching.precacheAndRoute([...]);
// 在 worker 中:
self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting());
});Workbox 的 clientsClaim() 也有助于页面在激活后立即受控。
下一步
- 审计当前 service worker 更新流:是手动还是自动?
- 确定应用风险剖面:多少进行中操作会中断?
- 设计更新 UX 模式:提示?横幅?应用内模态框?
- 实现客户端与 worker 之间的消息传递模式。
- 在真实设备上测试打开多标签的场景。
目标不是速度本身——而是在不干扰用户工作的前提下,在必要时交付更新。skipWaiting() 是工具,而非默认。