深入理解 Service Worker
生命周期、缓存策略、离线应用、推送通知、Background Sync 与 Workbox 实战
什么是 Service Worker?
定义:Service Worker 是浏览器提供的一种可编程的网络代理,运行在独立于主线程的后台线程中。它拦截页面发出的所有网络请求,开发者可以通过编程决定如何响应——从缓存返回、从网络获取、或生成自定义响应。它是 PWA(渐进式 Web 应用)的核心技术。
涉及场景:
- 离线应用:缓存关键资源,在无网络时仍能正常使用(如 Google Docs 离线编辑)
- 缓存策略:对不同类型的资源(静态文件、API、图片)应用不同的缓存策略
- 推送通知:即使页面关闭,仍可接收服务端推送并显示系统通知
- 后台同步:离线时提交的表单/消息在恢复网络后自动发送
- 预加载 / 预缓存:提前缓存下一页的资源,加速页面跳转
- CDN 回退:CDN 不可用时自动切换到备用源
作用:
- 网络弹性:让 Web 应用在弱网和离线环境下依然可用
- 性能提升:缓存命中时响应速度接近原生应用(0ms 延迟)
- 用户留存:推送通知让 Web 应用具备原生 App 级别的用户触达能力
- 面试重点:生命周期(install → waiting → activate)、缓存策略选择、更新机制是常见考题
浏览器架构中的 Service Worker:
┌─────────────────────────────────────────────┐
│ 主线程(Main Thread) │
│ ├── DOM、JS执行、事件处理 │
│ └── navigator.serviceWorker │
├─────────────────────────────────────────────┤
│ Service Worker 线程(独立线程) │
│ ├── 无法访问 DOM │
│ ├── 通过 postMessage 与主线程通信 │
│ ├── 拦截 fetch 请求 │
│ ├── 管理 Cache Storage │
│ └── 接收 push / sync 事件 │
├─────────────────────────────────────────────┤
│ 网络(Network) │
└─────────────────────────────────────────────┘前置条件
javascript
// 1. 必须使用 HTTPS(localhost 除外)
// 2. 浏览器支持检测
if ('serviceWorker' in navigator) {
// 支持 Service Worker
}生命周期
注册 → 安装(install)→ 等待(waiting)→ 激活(activate)→ 运行 → 销毁
↑ │
└────────── 新版本等待中 ──────────────┘
详细流程:
1. 页面调用 register()
2. 浏览器下载 SW 脚本
3. 解析并执行 → 触发 install 事件
4. install 成功 → 进入 waiting 状态(如果有旧 SW 在运行)
5. 旧 SW 控制的所有页面关闭后 → 新 SW 激活
6. 触发 activate 事件
7. SW 开始控制页面,监听 fetch/push/sync 等事件
8. 空闲一段时间后 → SW 线程被终止(下次事件触发时重新启动)注册
javascript
// main.js
async function registerSW() {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/', // 控制范围(默认为 SW 文件所在目录)
type: 'module', // 支持 ES Modules(新)
updateViaCache: 'none' // 不使用 HTTP 缓存检查更新
});
console.log('SW 注册成功,scope:', registration.scope);
// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
console.log('新 SW 状态:', newWorker.state);
// installing → installed → activating → activated
});
});
// 手动检查更新
await registration.update();
} catch (err) {
console.error('SW 注册失败:', err);
}
}
// registration 对象属性
// registration.installing — 正在安装的 SW
// registration.waiting — 等待激活的 SW
// registration.active — 当前激活的 SW
// registration.scope — 控制范围
// registration.updateViaCache — 更新策略安装(install)
javascript
// sw.js
const CACHE_NAME = 'app-v1';
const PRECACHE_URLS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html'
];
self.addEventListener('install', (event) => {
console.log('SW 安装中...');
// event.waitUntil() 确保异步操作完成后才进入下一阶段
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('预缓存资源');
return cache.addAll(PRECACHE_URLS);
})
);
// 跳过等待,立即激活(谨慎使用)
// self.skipWaiting();
});激活(activate)
javascript
self.addEventListener('activate', (event) => {
console.log('SW 激活中...');
// 清理旧缓存
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => {
console.log('删除旧缓存:', name);
return caches.delete(name);
})
);
})
);
// 立即接管所有页面(不等待页面刷新)
// self.clients.claim();
});拦截请求(fetch)
javascript
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
// 缓存命中 → 返回缓存
if (cachedResponse) {
return cachedResponse;
}
// 缓存未命中 → 发起网络请求
return fetch(event.request);
})
);
});缓存策略
1. Cache First(缓存优先)
javascript
// 适用:静态资源(JS/CSS/图片/字体)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
// 缓存新资源
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
});
})
);
});2. Network First(网络优先)
javascript
// 适用:API 请求、频繁更新的内容
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then(response => {
// 网络成功 → 更新缓存
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => {
// 网络失败 → 返回缓存
return caches.match(event.request);
})
);
});3. Stale While Revalidate(返回缓存 + 后台更新)
javascript
// 适用:用户头像、文章内容(允许短暂不一致)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
});
// 优先返回缓存,同时在后台更新
return cached || fetchPromise;
});
})
);
});4. Cache Only
javascript
// 适用:纯离线应用
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request));
});5. Network Only
javascript
// 适用:不需要缓存的请求(如分析统计)
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});综合策略
javascript
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// API 请求 → Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
return;
}
// 静态资源 → Cache First
if (request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'image') {
event.respondWith(cacheFirst(request));
return;
}
// HTML 页面 → Network First + 离线回退
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).catch(() => caches.match('/offline.html'))
);
return;
}
// 其他 → Stale While Revalidate
event.respondWith(staleWhileRevalidate(request));
});Cache API
javascript
// 打开缓存
const cache = await caches.open('my-cache-v1');
// 添加资源
await cache.add('/api/data'); // fetch + put
await cache.addAll(['/a.js', '/b.css']); // 批量添加
// 手动存储
await cache.put(request, response); // 存储请求-响应对
await cache.put('/custom-key', new Response('data')); // 自定义键
// 查询
const response = await cache.match(request); // 精确匹配
const response2 = await cache.match(request, {
ignoreSearch: true, // 忽略 URL 查询参数
ignoreMethod: true, // 忽略请求方法
ignoreVary: true // 忽略 Vary 头
});
const responses = await cache.matchAll(request); // 所有匹配
// 删除
await cache.delete(request);
// 列出所有缓存的请求
const keys = await cache.keys();
// 管理缓存
const names = await caches.keys(); // 所有缓存名
const exists = await caches.has('my-cache'); // 检查是否存在
await caches.delete('old-cache'); // 删除整个缓存
const matched = await caches.match(request); // 跨所有缓存查找通信
javascript
// 主线程 → SW
navigator.serviceWorker.controller.postMessage({
type: 'SKIP_WAITING'
});
// SW → 主线程
// sw.js
self.addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
// 回复消息
event.source.postMessage({ type: 'REPLY', data: 'OK' });
});
// SW → 所有页面
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'UPDATE_AVAILABLE' });
});
});
// 主线程监听 SW 消息
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('来自 SW:', event.data);
});
// MessageChannel 双向通信
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
console.log('SW 回复:', event.data);
};
navigator.serviceWorker.controller.postMessage(
{ type: 'INIT_PORT' },
[channel.port2]
);推送通知(Push API)
javascript
// 1. 请求通知权限
const permission = await Notification.requestPermission();
// 'granted' | 'denied' | 'default'
// 2. 订阅推送
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // 必须显示通知
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 将 subscription 发送到服务器
// 3. SW 接收推送
// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title || '通知', {
body: data.body,
icon: '/icon-192.png',
badge: '/badge-72.png',
image: data.image,
vibrate: [100, 50, 100],
data: { url: data.url },
actions: [
{ action: 'open', title: '查看' },
{ action: 'dismiss', title: '忽略' }
],
tag: data.tag, // 相同 tag 的通知会替换
renotify: true, // 替换时重新提醒
requireInteraction: false // 是否需要用户手动关闭
})
);
});
// 4. 通知点击
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open') {
event.waitUntil(
clients.openWindow(event.notification.data.url || '/')
);
}
});Background Sync
javascript
// 主线程:注册同步任务
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-messages');
// sw.js:处理同步
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
// 从 IndexedDB 读取待发送的消息
const messages = await getQueuedMessages();
for (const msg of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(msg)
});
await removeFromQueue(msg.id);
} catch (err) {
// 失败时 SW 会自动重试
throw err;
}
}
}
// Periodic Background Sync(定期同步,需要权限)
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // 至少每24小时一次
});
// sw.js
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'update-content') {
event.waitUntil(updateContent());
}
});更新策略
javascript
// 检测 SW 更新并提示用户
// main.js
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!refreshing) {
refreshing = true;
window.location.reload();
}
});
async function checkForUpdate() {
const registration = await navigator.serviceWorker.ready;
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本已安装,提示用户更新
if (confirm('新版本可用,是否更新?')) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
}
});
});
}总结
Service Worker 核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 生命周期 │
│ • register → install → waiting → activate → fetch │
│ • skipWaiting() 跳过等待,clients.claim() 立即接管 │
│ • install 中预缓存,activate 中清理旧缓存 │
├──────────────────────────────────────────────────────────┤
│ 缓存策略 │
│ • Cache First:静态资源 │
│ • Network First:API、动态内容 │
│ • Stale While Revalidate:允许短暂不一致的内容 │
│ • Cache/Network Only:极端场景 │
├──────────────────────────────────────────────────────────┤
│ 核心 API │
│ • Cache API:open / add / put / match / delete │
│ • Fetch 拦截:event.respondWith() │
│ • 通信:postMessage / MessageChannel / clients │
│ • Push API + Notification API:推送通知 │
│ • Background Sync:离线操作队列 │
├──────────────────────────────────────────────────────────┤
│ 注意事项 │
│ • 必须 HTTPS(localhost 除外) │
│ • 不能访问 DOM,通过 postMessage 通信 │
│ • 空闲时线程被终止,不要依赖全局变量持久化 │
│ • scope 决定控制范围 │
└──────────────────────────────────────────────────────────┘