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#后台同步#推送通知