Service Worker 注册与生命周期:install、activate、fetch 事件、更新机制
4.1 原理概述
Service Worker 是浏览器在后台独立运行的一种脚本,能够拦截网络请求、管理缓存,从而实现离线缓存、消息推送、后台同步等功能。其生命周期是离线缓存设计的核心,主要包含 注册、安装(install)、激活(activate)、空闲(idle) 和 终止(terminate) 五个阶段。与应用业务相关的最关键事件是 install、activate 和 fetch。
核心角色:Service Worker 作为客户端与 REST API 之间的代理,通过监听事件决定是返回缓存还是发起网络请求,从而在无网络时提供离线体验。
4.2 生命周期与事件流
一个 Service Worker 从注册到接管页面,经历以下流程:
- 注册(Registration): 页面 JavaScript 调用
navigator.serviceWorker.register(),浏览器下载并解析脚本。 - 安装(install): 首次安装时触发 install 事件,通常在此预缓存静态资源(HTML、CSS、JS、API 响应模板等)。
- 等待激活(Waiting): 若已有旧版 SW 在控制页面,新版进入等待状态。
- 激活(activate): 新版本取代旧版本时触发,常用于清理旧缓存、迁移数据。
- 控制页面(Controlling): 激活后,SW 开始控制所有同源页面,监听 fetch 事件。
- 更新(Update): 浏览器定期或通过
updateViaCache策略检查脚本变更,进入新一轮安装—激活流程。
4.3 注册与生命周期事件实现
4.3.1 注册 Service Worker
在主线程(通常为页面 index.js)中注册 SW 脚本:
// 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered at scope:', registration.scope);
})
.catch(err => {
console.error('SW registration failed:', err);
});
}
提示:注册时需确保 SW 脚本存放在站点根目录或同源路径下,且响应头
Content-Type: text/javascript。
4.3.2 install 事件——预缓存策略
install 事件发生在 SW 首次下载成功时;通常用 event.waitUntil() 延长安装生命周期,直到缓存操作完成。若 promise 失败,安装被视为失败,SW 不会激活。
// sw.js
const CACHE_NAME = 'rest-api-cache-v1';
const PRECACHE_URLS = [
'/',
'/index.html',
'/api/templates',
'/offline.html'
];
self.addEventListener('install', event => {
console.log('SW installing…');
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(PRECACHE_URLS);
})
);
});
4.3.3 activate 事件——清理旧缓存
activate 事件在 SW 激活时触发;通常用于删除不符合当前版本命名规范的旧缓存,避免磁盘膨胀和重复数据。
self.addEventListener('activate', event => {
console.log('SW activating…');
const cacheWhitelist = [CACHE_NAME]; // 只保留当前版本
event.waitUntil(
caches.keys().then(keyList => {
return Promise.all(
keyList.map(key => {
if (!cacheWhitelist.includes(key)) {
console.log('Deleting old cache:', key);
return caches.delete(key);
}
})
);
}).then(() => self.clients.claim()) // 立即控制已打开的页面
);
});
注意:调用
self.clients.claim() 可使新激活的 SW 立即接管所有页面,否则需要页面重新加载才会受控。
4.3.4 fetch 事件——离线响应与网络优先策略
每次请求都会触发 fetch 事件;SW 决定是否使用缓存、回退网络或组合策略。以下实现 网络优先 策略,适用于 REST API 缓存:
self.addEventListener('fetch', event => {
const request = event.request;
// 仅处理 GET 请求(API 只缓存 GET)
if (request.method !== 'GET') return;
event.respondWith(
fetch(request)
.then(networkResponse => {
// 将最新响应存入缓存(排除非成功状态)
const clonedResponse = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, clonedResponse);
});
return networkResponse;
})
.catch(() => {
// 网络失败,尝试缓存
return caches.match(request).then(cached => {
if (cached) return cached;
// 回退离线页面
return caches.match('/offline.html');
});
})
);
});
4.4 更新机制
浏览器每次导航/每 24 小时检查 SW 字节变化。一旦发现新版本,生命周期再次从 install 开始。默认新版本会进入 waiting 状态,直到所有被控制的页面关闭。可通过 self.skipWaiting() 在安装完成后立即激活:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});
同时在页面监听 controllerchange 事件,提示用户刷新:
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('新版 SW 已激活,建议刷新页面');
// 可在此弹出提示或自动刷新
window.location.reload();
});
4.5 调试方法
| 工具/方式 | 操作步骤 |
|---|---|
| Chrome DevTools → Application | 打开 "Service Workers" 面板,可查看注册状态、点击 "Unregister"、"Update"、"Push" 测试。 |
| Cache Storage 面板 | 查看所有缓存名称和内容,验证缓存是否按预期生成。 |
| Network 面板 | 查看请求是否由 Service Worker 响应(响应头显示 from ServiceWorker)。 |
| Console 日志 | SW 内部的 console.log 会输出到专用控制台(需在 DevTools 中开启 "Show console for service workers")。 |
| Offline 模拟 | 在 Network 面板切换 "Offline" 模式,验证离线缓存是否生效。 |
4.6 部署注意事项
- 脚本不可跨域:Service Worker 文件必须与页面同源(或通过
Access-Control-Allow-Origin允许),scope 默认是脚本路径的父目录。 - HTTPS 强制:生产环境必须使用 HTTPS(localhost 除外)。
- 缓存版本管理:每次更新时修改
CACHE_NAME(如rest-api-cache-v2),激活事件中清理旧版本。 - 注册时机:不要在页面生命周期前注册 SW,建议在
DOMContentLoaded或load事件中进行。
4.7 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| SW 安装失败,控制台报 404 | sw.js 路径错误或未部署 | 检查注册路径,确认文件存在。 |
| 缓存未生效,请求仍走网络 | fetch 事件未正确响应,或策略未匹配请求 | 检查 event.respondWith 是否执行,打印请求 URL。 |
| 旧缓存未清理,磁盘占用大 | activate 事件中未实现缓存清理逻辑 | 在 activate 中遍历缓存名,删除不在白名单的缓存。 |
| 新版 SW 不激活,一直等待 | 未调用 self.skipWaiting(),且页面未关闭 | 在 install 末尾调用 self.skipWaiting()。 |
| 跨域 API 请求不被缓存 | fetch 事件没有处理 opaque 请求 | 对跨域请求使用 no-cors 模式,并谨慎处理缓存策略。 |
4.8 性能与稳定性优化
- 细粒度缓存版本控制:将静态资源与 API 响应分入不同缓存(如
static-v1、api-v1),避免全量清理。 - 使用
stale-while-revalidate策略:先返回缓存,后台发起请求更新缓存,减少等待时间。 - 限制缓存容量:定期检查缓存大小并删除最早项,避免超出浏览器配额(尤其是移动端)。
- 减少 install 预缓存数量:只缓存关键页面和核心 API 模板,其他资源按需缓存。
- 配合
updateViaCache配置:注册时可设置updateViaCache: 'none'避免浏览器缓存 SW 脚本。
// 示例:stale-while-revalidate 策略
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(network => {
// 更新缓存
caches.open(CACHE_NAME).then(cache => cache.put(event.request, network.clone()));
return network;
}).catch(() => cached); // 网络失败返回缓存
return cached || fetchPromise;
})
);
});
推荐实践:在开发阶段,使用 DevTools 的 "Update on reload" 选项(Application → Service Workers)每次刷新都重新注册 SW,加速调试。
同时保留
同时保留
skipWaiting() 和 clients.claim() 便于迭代测试。