Service Worker 筆記
Created: 2025/8/15
Updated: 2025/8/17
記錄了一些關於service worker的特性和一些執行流程
1. 基本概念
Service Worker (SW) 是一個在瀏覽器背景執行的腳本,作為網頁與瀏覽器之間的代理服務器。
核心特性
- 獨立線程運行:不會阻塞主線程 UI 操作
- 事件驅動模式:基於事件監聽和響應
- HTTPS 限制:出於安全考量,僅在 HTTPS 或 localhost 環境下工作
- 作用域控制:只能控制其註冊路徑下的頁面
- 無 DOM 訪問:無法直接操作 DOM,需透過 postMessage 通信
- 可程式化網路代理:可攔截和處理網路請求
核心概念架構
┌─────────────────────────────────────────────────────────┐
│ Browser Process │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Web Page │◄──────►│ Service Worker │ │
│ │ (主線程) │ │ (獨立線程) │ │
│ └──────────────┘ └──────────────────────┘ │
│ ▲ │ │
│ │ │ Intercept │
│ │ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Cache │◄───────│ Network Request │ │
│ │ Storage │ │ │ │
│ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2. 生命週期
完整生命週期流程
┌─────────────┐
│ 未註冊 │
└──────┬──────┘
│ navigator.serviceWorker.register()
▼
┌─────────────┐
│ Parsing │ ← SW 腳本解析
└──────┬──────┘
│ 解析成功
▼
┌─────────────┐
│ Installing │ ← install event 觸發
└──────┬──────┘
│ 安裝完成
▼
┌─────────────┐
│ Waiting │ ← 等待舊版本退出
└──────┬──────┘
│ 舊版本退出或 skipWaiting()
▼
┌─────────────┐
│ Activating │ ← activate event 觸發
└──────┬──────┘
│ 激活完成
▼
┌─────────────┐
│ Activated │ ← 開始控制頁面
│ (Active) │ ← fetch/message/sync/push events
└──────┬──────┘
│ 新版本註冊
▼
┌─────────────┐
│ Redundant │ ← 被新版本取代
└─────────────┘
生命週期階段說明
-
Registration (註冊)
- 時機:頁面 JavaScript 執行時
- 觸發:
navigator.serviceWorker.register()
- 作用:下載並解析 SW 腳本
-
Installation (安裝)
- 時機:首次註冊或檢測到新版本
- 事件:install event
- 作用:預緩存靜態資源
-
Waiting (等待)
- 時機:新版本安裝完成但舊版本仍在控制頁面
- 作用:等待所有使用舊版本的頁面關閉
- 跳過:
self.skipWaiting()
可強制激活
-
Activation (激活)
- 時機:沒有頁面使用舊版本或調用 skipWaiting()
- 事件:activate event
- 作用:清理舊緩存,接管頁面控制權
-
Active (運行中)
- 時機:激活完成後
- 事件:fetch, message, sync, push 等
- 作用:攔截請求、推送通知、背景同步
3. Service Worker 介入時機與場景分析
兩個關鍵介入時機
時機 1:首次註冊(背景安裝)
前一步驟:JavaScript 執行階段
├─ DOM 已經構建完成
├─ <script> 標籤正在執行
├─ 頁面的主要 JavaScript 代碼運行中
└─ 此時頁面已經可以與用戶互動
↓
↓ 如何介入:開發者在 JS 中調用註冊 API
↓
┌─────────────────────────────────────────────┐
│ 🔴 Service Worker 註冊點 │
│ │
│ // 通常在 DOMContentLoaded 或 load 事件中 │
│ navigator.serviceWorker.register('/sw.js') │
│ │
│ 介入影響: │
│ • 下載並解析 sw.js(背景執行,不阻塞頁面) │
│ • 觸發 install 事件 │
│ • 建立緩存存儲 │
│ • 預載入關鍵資源 │
└─────────────────────────────────────────────┘
↓
↓ 註冊後的影響
↓
最後步驟:頁面完成載入(Load Event)
├─ Service Worker 在背景安裝中
├─ 當前頁面不受 SW 控制(需刷新)
├─ 頁面正常完成載入流程
└─ SW 狀態:Installing → Installed → Waiting
- 前一步驟:瀏覽器正在執行頁面的 JavaScript,此時 DOM 已經準備好,可以操作頁面元素
- 介入點:JavaScript 執行階段
- 影響:背景下載並安裝,不阻塞當前頁面
- 結果:需刷新頁面才能讓 SW 控制
時機 2:控制頁面後(攔截請求)
前一步驟:用戶導航動作
├─ 用戶在地址欄輸入 URL
├─ 用戶點擊頁面連結
├─ 用戶刷新頁面(F5)
└─ JavaScript 觸發導航(location.href)
↓
↓ 如何介入:瀏覽器自動將請求轉發給 SW
↓
┌─────────────────────────────────────────────┐
│ 🔴 Service Worker 攔截點 │
│ │
│ self.addEventListener('fetch', event => { │
│ // 在網路請求發出前攔截,此時可以決定: │
│ // 1. 返回緩存(超快) │
│ // 2. 發送網路請求 │
│ // 3. 返回自定義響應 │
│ }) │
│ │
│ 介入影響: │
│ • 完全繞過 DNS 查詢 │
│ • 跳過 TCP/TLS 握手 │
│ • 不需要發送 HTTP 請求 │
│ • 直接從本地緩存返回 │
└─────────────────────────────────────────────┘
↓
↓ 攔截後的處理
↓
最後步驟:返回響應 → 頁面渲染
├─ 緩存命中:立即返回資源(<10ms)
├─ 緩存未中:降級到網路請求
├─ 混合策略:返回緩存同時更新
└─ 頁面接收響應後正常解析渲染
- 前一步驟:用戶觸發導航,瀏覽器準備發起網路請求
- 介入點:瀏覽器準備發起網路請求時
- 影響:完全繞過 DNS/TCP/TLS,直接從緩存返回
- 結果:載入速度提升 80-90%
三種典型場景對比
場景對比表
載入階段 | 無 SW | 首次訪問(註冊 SW) | SW 已控制 |
---|---|---|---|
DNS 查詢 | 50-100ms | 50-100ms | 0ms |
TCP 握手 | 50-100ms | 50-100ms | 0ms |
TLS 握手 | 100-200ms | 100-200ms | 0ms |
HTTP 請求 | 100-200ms | 100-200ms | 0ms |
等待響應 | 100-500ms | 100-500ms | 1-10ms |
下載內容 | 200-1000ms | 200-1000ms | 5-20ms |
總時間 | 600-2100ms | 600-2100ms | 6-30ms |
SW 狀態 | 無 | 背景安裝中 | 完全控制 |
詳細流程對比
4. 事件監聽器
常見的事件分類
Service Worker 提供多種事件類型來處理不同的應用場景,以下是常見事件的分類和使用說明:
- 生命週期事件:install, activate
- 功能性事件:fetch, push, sync, periodicsync
- 通知事件:notificationclick, notificationclose
- 通信事件:message, messageerror
- 實驗性事件:contentdelete, backgroundfetch
事件名稱 | 使用頻率 | 主要用途 | 說明 |
---|---|---|---|
install | 高 | 初始安裝 | SW 首次安裝時觸發,用於快取資源初始化 |
activate | 高 | 版本更新 | SW 啟用時觸發,用於清理舊快取和版本管理 |
fetch | 高 | 網路代理 | 攔截所有網路請求,實現快取策略和離線功能 |
sync | 中 | 背景同步 | 網路恢復時同步離線期間的資料 |
periodicsync | 低 | 定期同步 | 定期在背景執行資料同步(實驗性功能) |
push | 中 | 推播通知 | 接收伺服器推送的通知訊息 |
notificationclick | 低 | 通知互動 | 處理用戶點擊通知的行為 |
notificationclose | 低 | 通知關閉 | 處理用戶關閉通知的行為 |
message | 中 | 訊息傳遞 | 與主執行緒或其他 SW 進行雙向通信 |
messageerror | 低 | 錯誤處理 | 處理訊息傳遞過程中的錯誤 |
contentdelete | 低 | 內容刪除 | 處理快取內容的刪除請求(實驗性) |
backgroundfetch | 低 | 背景下載 | 處理大型檔案的背景下載(實驗性) |
使用優先級建議
- 必須實作(高頻率):
install
、activate
、fetch
- 推薦實作(中頻率):
message
、push
、sync
- 選擇性實作(低頻率):通知相關事件、實驗性事件
注意事項:
- 生命週期事件:確保正確的版本管理和快取策略
- 功能性事件:
fetch
是核心功能,需謹慎處理以避免影響效能 - 通知事件:需要用戶授權才能使用
- 實驗性事件:瀏覽器支援度有限,使用前需檢查相容性
核心事件實作
1. 生命週期事件
// install - 預緩存資源
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/styles.css',
'/script.js',
'/offline.html'
]);
})
);
// 可選:跳過等待
self.skipWaiting();
});
// activate - 清理舊緩存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== 'v1') {
return caches.delete(cacheName);
}
})
);
})
);
// 立即控制所有客戶端
return self.clients.claim();
});
2. 功能性事件
// fetch - 攔截網路請求
self.addEventListener('fetch', event => {
const { request } = event;
// 根據資源類型處理
if (request.destination === 'document') {
event.respondWith(handlePageRequest(request));
} else if (request.url.includes('/api/')) {
event.respondWith(handleApiRequest(request));
} else {
event.respondWith(
caches.match(request).then(response => {
return response || fetch(request);
})
);
}
});
// push - 推送通知
self.addEventListener('push', event => {
const options = {
body: event.data ? event.data.text() : 'New notification',
icon: '/icon-192.png',
badge: '/badge-72.png'
};
event.waitUntil(
self.registration.showNotification('Push Notification', options)
);
});
// sync - 背景同步
self.addEventListener('sync', event => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
5. 緩存策略
根據我們的需求,我們能選擇各種不同的緩存策略,sevice worker有三種策略: Cache First、Stale-While-Revalidate以及Network First
主要策略實作
1. Cache First (緩存優先)
適用:靜態資源(CSS, JS, 圖片)
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open('static-v1');
cache.put(request, response.clone());
return response;
}
2. Network First (網路優先)
適用:API 請求、動態內容
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open('dynamic-v1');
cache.put(request, response.clone());
return response;
} catch (error) {
return caches.match(request);
}
}
3. Stale While Revalidate
適用:頻繁更新但可接受舊版本的內容
async function staleWhileRevalidate(request) {
const cached = await caches.match(request);
const fetchPromise = fetch(request).then(response => {
const cache = await caches.open('revalidate-v1');
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
6. 實作指南
基本實作步驟
// 1. 在主頁面註冊 SW
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.log('SW registration failed'));
});
}
// 2. 在 sw.js 實作基本功能
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/styles.css',
'/script.js'
];
// 安裝並預緩存
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// 清理舊版本
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 處理請求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
最佳實踐
- 版本管理
const VERSION = '1.0.0';
const CACHE_NAME = `app-v${VERSION}`;
- 緩存大小控制
async function limitCacheSize(name, size) {
const cache = await caches.open(name);
const keys = await cache.keys();
if (keys.length > size) {
cache.delete(keys[0]);
}
}
- 更新通知
self.addEventListener('activate', event => {
event.waitUntil(
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'SW_UPDATED' });
});
})
);
});
7. 常見問題
Q1: Service Worker 不更新?
- 更改 sw.js 檔案內容(即使是註釋)
- 使用
registration.update()
強制更新 - 在 Chrome DevTools 勾選 "Update on reload"
Q2: 緩存過多佔用空間?
- 設定緩存過期時間
- 限制緩存數量
- 定期清理舊版本緩存
Q3: 如何偵錯?
Chrome DevTools
├── Application Tab
│ ├── Service Workers (狀態查看)
│ ├── Cache Storage (緩存檢視)
│ └── Clear Storage (清理)
├── Network Tab
│ └── Check "Offline" (離線測試)
└── Console
└── SW Logs (除錯訊息)
8. 總結
Service Worker 是實現 Progressive Web App 的核心技術,通過理解其生命週期、掌握 API 使用、選擇適當的緩存策略,可以顯著提升 Web 應用的效能和用戶體驗。
關鍵要點:
- 理解執行時機:從註冊到控制頁面的完整流程
- 選擇緩存策略:根據資源類型選擇合適策略
- 管理版本更新:確保用戶獲得最新版本
- 監控與優化:持續監控緩存命中率和載入效能
Service Worker 讓 Web 應用擁有了原生應用般的體驗,是現代 Web 開發不可或缺的技術。