Service Worker 进阶:离线优先架构与后台同步策略
技术架构(更新于 2026年6月4日)
Service Worker:离线优先架构的核心
Service Worker 是浏览器中独立于页面的后台线程,能拦截网络请求、管理缓存、处理推送:
| 特性 | 说明 |
|---|---|
| 运行环境 | 独立 Worker 线程,无 DOM 访问 |
| 生命周期 | install → activate → fetch |
| 作用域 | 注册路径及其子路径 |
| 通信 | postMessage / MessageChannel |
| 持久化 | 事件驱动,不常驻内存 |
生命周期详解
┌──────────┐
│ parsed │
└────┬─────┘
│ install()
┌────▼─────┐
┌─────│installing│
│ └────┬─────┘
│ │ waitUntil()
│ ┌────▼─────┐
│ │ installed │ (waiting)
│ └────┬─────┘
│ │ activate()
│ ┌────▼─────┐
└────▶│activating│
└────┬─────┘
│
┌────▼─────┐
│ activated │ ← 处理 fetch/push/sync
└──────────┘
注册与更新
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
updateViaCache: 'none'
});
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
showToast('新版本已就绪,刷新生效');
}
});
});
}
Cache API:缓存策略矩阵
五大经典策略
| 策略 | 网络优先 | 适用场景 | 离线行为 |
|---|---|---|---|
| Network Only | ✅ | 非缓存数据(支付/实时) | 失败 |
| Network First | ✅+降级 | 频繁更新的 API | 返回缓存 |
| Cache First | ❌ | 不变资源(字体/图片) | 返回缓存 |
| Cache Only | ❌ | 预缓存的离线页面 | 返回缓存 |
| Stale While Revalidate | 后台更新 | HTML/非关键 API | 返回旧缓存 |
策略实现
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/offline.html',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
)
)
);
});
Cache First 实现
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
}
Network First 实现
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
return caches.match('/offline.html');
}
}
Stale While Revalidate 实现
async function staleWhileRevalidate(request: Request): Promise<Response> {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
路由分发
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== 'GET') return;
if (url.pathname.startsWith('/api/')) {
event.respondWith(staleWhileRevalidate(request));
} else if (url.pathname.match(/\.(css|js|woff2)$/)) {
event.respondWith(cacheFirst(request));
} else if (url.pathname.match(/\.(png|jpg|webp|svg)$/)) {
event.respondWith(cacheFirst(request));
} else {
event.respondWith(networkFirst(request));
}
});
Background Sync:后台同步
注册同步任务
// 页面中注册
async function registerSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('submit-pending-data');
}
// 离线时暂存数据
async function saveOffline(data: object) {
const db = await openDB('outbox', 1, {
upgrade(db) {
db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
}
});
await db.add('pending', data);
await registerSync();
}
Service Worker 处理同步
self.addEventListener('sync', (event) => {
if (event.tag === 'submit-pending-data') {
event.waitUntil(submitPendingData());
}
});
async function submitPendingData() {
const db = await openDB('outbox', 1);
const pending = await db.getAll('pending');
for (const item of pending) {
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(item),
});
await db.delete('pending', item.id);
} catch {
throw new Error('同步失败,将重试');
}
}
}
Push API:推送通知
订阅推送
async function subscribePush(publicKey: string) {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
});
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription.toJSON())
});
}
处理推送
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: '新消息', body: '' };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/notification.png',
badge: '/icons/badge.png',
vibrate: [200, 100, 200],
data: { url: data.url },
actions: [
{ action: 'open', title: '查看' },
{ action: 'dismiss', title: '忽略' }
]
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.url || '/';
if (event.action === 'dismiss') return;
event.waitUntil(
self.clients.matchAll({ type: 'window' }).then((clients) => {
for (const client of clients) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
return self.clients.openWindow(url);
})
);
});
Workbox:Google 官方工具链
import { registerRoute } from 'workbox-routing';
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 }),
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
})
);
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api',
plugins: [
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }),
],
})
);
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
})
);
缓存策略对比
| 策略 | 首次加载 | 后续加载 | 离线 | 数据新鲜度 | 复杂度 |
|---|---|---|---|---|---|
| Network Only | 网络 | 网络 | ❌ | 最新 | 低 |
| Network First | 网络 | 网络/缓存 | ✅ | 较新 | 中 |
| Cache First | 网络 | 缓存 | ✅ | 可能旧 | 低 |
| Stale While Revalidate | 网络 | 缓存 | ✅ | 最终一致 | 中 |
浏览器兼容性
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Service Worker | 40+ | 44+ | 11.1+ | 17+ |
| Cache API | 40+ | 41+ | 11.1+ | 17+ |
| Background Sync | 49+ | 不支持 | 不支持 | 17+ |
| Push API | 50+ | 44+ | 16+ | 17+ |
| Navigation Preload | 62+ | 不支持 | 不支持 | 17+ |
最佳实践
- 预缓存关键资源:install 时缓存 App Shell,保证离线可用
- 版本化缓存名:
app-v1→app-v2,activate 时清理旧缓存 - 路由分发:不同资源类型使用不同策略
- 限制缓存大小:ExpirationPlugin 防止缓存无限增长
- 优雅降级:SW 注册失败时页面仍正常工作
总结
Service Worker 是构建离线优先 Web 应用的核心,通过 Cache API 的策略矩阵实现网络与缓存的智能调度,Background Sync 保证离线操作的最终一致性,Push API 实现服务端主动通知。Workbox 工具链大幅降低了实现复杂度。
本站提供浏览器本地工具,免注册即可试用 →
#Service Worker#离线优先#Cache API#后台同步#推送通知