在 TypeScript 中使用 Service Worker
本文章將說明如何在 TypeScript 中使用 Service Worker。
我們將說明如何在 TypeScript 中使用 Service Worker,並包含實用範例。
YouTube Video
在 TypeScript 中使用 Service Worker
Service Worker 是一種介於瀏覽器與網路之間的“請求代理”。它實現了請求攔截、快取管理、離線支援,以及後台處理(同步與推播)。使用 TypeScript 可以增加型別安全性並提升可維護性。
TypeScript 設定方法
tsconfig.json(啟用 WebWorker 型別)
我們來看看在 tsconfig.json 中啟用 WebWorker 型別的範例。
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "module": "ES2020",
5 "lib": ["ES2020", "WebWorker"],
6 "moduleResolution": "Bundler",
7 "strict": true,
8 "noEmitOnError": true,
9 "outDir": "out",
10 "skipLibCheck": true
11 },
12 "include": ["sw.ts"]
13}- 在
lib陣列中加入WebWorker,即可使用如ServiceWorkerGlobalScope這類型別。 DOM與WebWorker擁有不同的型別,因此通常會將瀏覽器(主應用)與Service Worker的tsconfig.json設定分開管理。Service Worker檔案最終會輸出到與範圍(scope)匹配的路徑(通常是網站根目錄/sw.js)。- 出於安全考量,Service Worker 只能在 HTTPS(或
localhost)環境下運作。
瀏覽器端(主程式)註冊程式碼
register-sw.ts
1// register-sw.ts
2async function registerServiceWorker() {
3 if (!('serviceWorker' in navigator)) return;
4
5 try {
6 const registration = await navigator.serviceWorker.register(
7 '/sw.js', { scope: '/' }
8 );- 這個流程會註冊一個
Service Worker。scope指的是Service Worker可以控制的路徑範圍。例如,如果你將/sw.js放在網站根目錄並將scope設定為根目錄(/),那麼你可以控制整個網站的所有資源。另一方面,如果你指定一個特定的目錄,例如/app/,那麼只有該目錄下的內容會被控制。
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }waiting表示 新的 Service Worker 已安裝完畢,正在等待啟動 的狀態。在這個階段,現有的頁面仍然由舊的Service Worker控制,因此通常會提示用戶確認,獲得同意後再呼叫skipWaiting()來立即啟動新的Service Worker。這樣可以讓最新的處理流程立即生效,而不必等待下次頁面重新載入。
1 // When a new SW is installing, monitor its state changes
2 registration.addEventListener('updatefound', () => {
3 const newWorker = registration.installing;
4 if (!newWorker) return;
5 newWorker.addEventListener('statechange', () => {
6 if (newWorker.state === 'installed' &&
7 navigator.serviceWorker.controller) {
8 // New content available, prompt the user
9 promptUserToUpdate(registration);
10 }
11 });
12 });updatefound事件在新的 Service Worker 安裝開始時被觸發。當這個事件發生時,新的 worker 會被設置在registration.installing,因此通過監聽它的statechange,你可以檢測安裝什麼時候完成(installed)。此外,如果存在navigator.serviceWorker.controller,則表示舊的 Service Worker 已經在控制該頁面,此時可以通知用戶有新版本存在。
1 // When the active worker changes (e.g., after skipWaiting), reload if desired
2 navigator.serviceWorker.addEventListener('controllerchange', () => {
3 // Optionally reload to let the new SW take over
4 window.location.reload();
5 });
6 } catch (err) {
7 console.error('Service Worker registration failed: ', err);
8 }
9}controllerchange事件會在新的 Service Worker 開始控制當前頁面時被觸發。此時重新載入頁面即可立即套用新的快取策略和處理流程。然而,自動重新載入可能會降低用戶體驗,因此 最好是在取得用戶同意後再重新載入。
1function promptUserToUpdate(reg: ServiceWorkerRegistration) {
2 // Show UI to user. If user accepts:
3 if (reg.waiting) {
4 reg.waiting.postMessage({ type: 'SKIP_WAITING' });
5 }
6}
7
8registerServiceWorker();- 通過讓
Service Worker從客戶端接收到postMessage({ type: 'SKIP_WAITING' })並呼叫self.skipWaiting(),你可以促使更新立即執行。
sw.ts 中的範圍宣告
接下來,讓我們看看典型的 Service Worker 實現 app shell 快取的範例。
在 TypeScript 中使用 Service Worker 時,將正確型別賦予 self 是很有用的。
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;- 在 TypeScript 中,
self預設被視為any,因此若沒有額外註明型別,您將無法獲得如skipWaiting()或clients這類 Service Worker 專有 API 的型別提示與檢查。 - 指定
ServiceWorkerGlobalScope型別可啟用自動完成功能,防範錯誤使用,使開發更安全並與常規 DOM 腳本區隔。
基本 Service Worker(安裝/啟用/攔截請求)
本範例包含快取版本管理、安裝時預先快取、啟用時刪除舊快取,以及各種攔截請求的快取策略(如靜態檔案採 cache-first,API 採 network-first)。
sw.ts(最精簡設置與快取骨架)
1const CACHE_NAME = 'app-shell-v1';
2const STATIC_ASSETS = [
3 '/',
4 '/index.html',
5 '/styles.css',
6 '/main.js',
7 '/fallback.png'
8];
9
10self.addEventListener('install', (event: ExtendableEvent) => {
11 // Pre-cache application shell
12 event.waitUntil(
13 caches.open(CACHE_NAME)
14 .then(cache => cache.addAll(STATIC_ASSETS))
15 // Activate immediately (optional: coordinate with client)
16 .then(() => self.skipWaiting())
17 );
18});- 在
install事件期間,應用程式的 靜態資源(App Shell) 會被預先快取。透過呼叫self.skipWaiting(),新的Service Worker會立即啟動,讓最新的快取無需等到下一次存取時就能生效。
1self.addEventListener('activate', (event: ExtendableEvent) => {
2 // Clean up old caches and take control of clients immediately
3 event.waitUntil(
4 caches.keys().then(keys =>
5 Promise.all(keys
6 .filter(key => key !== CACHE_NAME)
7 .map(key => caches.delete(key)))
8 ).then(() => self.clients.claim())
9 );
10});- 在
activate事件中,舊版本的快取會被刪除,並且可確保Service Worker保持最新狀態。此外,透過呼叫self.clients.claim(),新的Service Worker能在不用重新載入頁面的情況下控制所有用戶端。
1self.addEventListener('fetch', (event: FetchEvent) => {
2 const request = event.request;
3 const url = new URL(request.url);
4
5 // Navigation requests (SPA) -> network-first with fallback to cached index.html
6 if (request.mode === 'navigate') {
7 event.respondWith(
8 fetch(request).catch(() => caches.match('/index.html') as Promise<Response>)
9 );
10 return;
11 }
12
13 // Simple API routing: network-first for /api/
14 if (url.pathname.startsWith('/api/')) {
15 event.respondWith(networkFirst(request));
16 return;
17 }
18
19 // Static assets: cache-first
20 event.respondWith(cacheFirst(request));
21});- 在
fetch階段,你可以 攔截請求並控制回應內容。你可以實作如 cache-first 或 network-first 之類的策略,幫助提升離線支援與效能。
1self.addEventListener('message', (event: ExtendableMessageEvent) => {
2 const data = (event as any).data;
3 if (!data) return;
4
5 if (data.type === 'SKIP_WAITING') {
6 // Force the waiting service worker to become active
7 self.skipWaiting();
8 }
9});- 收到
SKIP_WAITING時,呼叫self.skipWaiting()可以立即啟動等待中的 Service Worker。因此,新的版本會在下次請求時自動套用,而無需重新載入頁面。
實用快取策略總覽
cache-first
Cache-first(快取優先) 會先查詢快取,若有則直接回應。若沒有則從網路取得,並存進快取。適合靜態檔案。
1async function cacheFirst(request: Request): Promise<Response> {
2 const cache = await caches.open(CACHE_NAME);
3 const cached = await cache.match(request);
4 if (cached) {
5 return cached;
6 }
7 const response = await fetch(request);
8 if (response && response.ok) {
9 cache.put(request, response.clone());
10 }
11 return response;
12}- 本段程式碼示範快取優先的實作方法。若有快取則直接回應,否則從網路取得並存入快取。適合不常變動的靜態資源,如圖片或 CSS。
network-first
Network-first(網路優先) 則優先從網路取得資料,失敗時才從快取讀取。適合需要最新資料的 API。
1async function networkFirst(request: Request): Promise<Response> {
2 const cache = await caches.open(CACHE_NAME);
3 try {
4 const response = await fetch(request);
5 if (response && response.ok) {
6 cache.put(request, response.clone());
7 }
8 return response;
9 } catch (err) {
10 const cached = await cache.match(request);
11 if (cached) return cached;
12 return new Response(JSON.stringify({ error: 'offline' }), {
13 status: 503,
14 headers: { 'Content-Type': 'application/json' }
15 });
16 }
17}- 本段程式碼示範網路優先的實作方式。若取得網路回應則存入快取,若失敗則回傳快取內容。適用於需要及時資料的內容,如新聞文章或 API 回應。
stale-while-revalidate
stale-while-revalidate 會先回傳快取內容,然後在背景同時從網路更新快取。可兼顧回應速度和資料新鮮度。
1async function staleWhileRevalidate(request: Request, cacheName = CACHE_NAME): Promise<Response> {
2 const cache = await caches.open(cacheName);
3 const cachedResponse = await cache.match(request);
4 const networkFetch = fetch(request).then(networkResponse => {
5 if (networkResponse && networkResponse.ok) {
6 cache.put(request, networkResponse.clone());
7 }
8 return networkResponse;
9 }).catch(() => undefined);
10
11 // Return cached immediately if exists, otherwise wait network
12 return cachedResponse || (await networkFetch) || new Response('offline', { status: 503 });
13}- 本範例會在有快取時立即回應,同時背景從網路取回新資料以更新快取。可快速回應使用者,並於下次使用最新內容,適合 UI 資源或輕量級資料傳遞。
優化更新流程(通知與安全重新載入)
Service Worker 的更新並非立即生效;新的版本會一直處於等待狀態直到現有的分頁被關閉。
這裡我們實作一套機制,讓新版本準備就緒時能通知用戶端,並可依使用者操作安全重新載入頁面。
當新版本已準備好時,請從 Service Worker 端通知客戶端
1// In sw.ts: after 'activate' or when new version is ready, broadcast a message
2async function notifyClientsUpdated() {
3 const all = await self.clients.matchAll({ type: 'window' });
4 for (const client of all) {
5 client.postMessage({ type: 'SW_UPDATED' });
6 }
7}
8
9// e.g., call this at the end of 'activate'
10self.addEventListener('activate', (event) => {
11 event.waitUntil((async () => {
12 if ('navigationPreload' in self.registration) {
13 await self.registration.navigationPreload.enable();
14 }
15 // cache cleanup
16 const cacheNames = await caches.keys();
17 await Promise.all(
18 cacheNames.map((name) => {
19 if (name !== CACHE_NAME) {
20 return caches.delete(name);
21 }
22 })
23 );
24
25 await self.clients.claim();
26 await notifyClientsUpdated();
27 })());
28});- 在此範例中,
activate事件結束時會呼叫notifyClientsUpdated通知所有已連線客戶新版本已就緒。clients.claim()是一個可以立即讓目前開啟中的頁面(clients)由新啟動的 Service Worker 控制的方法。通常Service Worker只會在下次載入頁面時開始控制,但透過使用clients.claim(),可以讓頁面立即被控制,無須重新載入。
在用戶端顯示更新 UI,並依使用者操作重新載入
1// in app startup
2navigator.serviceWorker.addEventListener('message', (e) => {
3 if (e.data?.type === 'SW_UPDATED') {
4 // Show a non-intrusive toast or banner: "New version available"
5 // When user clicks "Reload", call:
6 window.location.reload();
7 }
8});- 客戶端透過
message事件收到SW_UPDATED並在 UI 顯示更新通知。當用戶選擇重新載入時,就會執行window.location.reload(),將頁面上的舊版 HTML、CSS 及其他資源更新到最新版本。這可確保透過clients.claim()切換控制權後的快取與Service Worker能反映到整個頁面中。
離線備援(Fallback)
準備 /offline.html 以供重要頁面導覽,並提供即使沒有圖片或字型也能傳達訊息的簡易 UI。若 API 請求失敗,盡可能顯示 最後一次快取狀態,並於背景重試,以提升用戶體驗。
實作範例
1// sw.ts
2const CACHE_NAME = 'app-cache-v1';
3
4// Cache offline.html during install
5self.addEventListener('install', (event) => {
6 event.waitUntil((async () => {
7 const cache = await caches.open(CACHE_NAME);
8 await cache.addAll(['/offline.html']);
9 })());
10});
11
12// Handle fetch requests
13self.addEventListener('fetch', (event) => {
14 const request = event.request;
15
16 // Navigation requests (e.g., page transitions)
17 if (request.mode === 'navigate') {
18 event.respondWith((async () => {
19 try {
20 // Try to fetch from the network as usual
21 return await fetch(request);
22 } catch (err) {
23 // On failure, return offline fallback page
24 const cache = await caches.open(CACHE_NAME);
25 return await cache.match('/offline.html') as Response;
26 }
27 })());
28 }
29});- 於
install階段預先快取/offline.html,確保網路無法連線時可顯示簡易畫面。 - 在
fetch事件中,您可以使用request.mode === 'navigate'來監控導覽請求,並針對頁面切換進行特殊處理。 - 若網路請求失敗時就回退至
/offline.html,即使離線也能顯示。
客戶端與 Service Worker 之間的通訊
由於 Service Worker 獨立於頁面生命週期運作,進行狀態通知與指令傳達的 雙向訊息溝通 變得非常重要。為訊息定義型別有助於避免錯誤的訊息發送,提供自動補全,也讓您的實作更加健全。
程式碼範例
- 訊息型別定義
1type SwToClient =
2 | { type: 'SW_READY' }
3 | { type: 'SW_UPDATED' }
4 | { type: 'CACHE_CLEARED' }
5 | { type: 'PING'; ts: number };
6
7type ClientToSw =
8 | { type: 'CLEAR_CACHE' }
9 | { type: 'PING'; ts: number };SwToClient為 Service Worker 傳送給客戶端的訊息型別。ClientToSw是客戶端發送給 Service Worker 的訊息型別。- 這可以讓你明確雙向通訊中可以交換的事件類型。
- Service Worker 端的處理
1self.addEventListener('message', (event) => {
2 const data = event.data as ClientToSw;
3 if (data?.type === 'CLEAR_CACHE') {
4 event.waitUntil((async () => {
5 const keys = await caches.keys();
6 await Promise.all(keys.map((k) => caches.delete(k)));
7 await broadcast({ type: 'CACHE_CLEARED' });
8 })());
9 } else if (data?.type === 'PING') {
10 event.source?.postMessage({ type: 'PING', ts: data.ts } as SwToClient);
11 }
12});- Service Worker 接收客戶端訊息,依型別分流處理。
- 對於
CLEAR_CACHE,將刪除快取,並以CACHE_CLEARED通知所有客戶端。 - 對於
PING,會回覆發送端一個包含時間戳記的PING訊息。
- 由 Service Worker 通知所有客戶端
1async function broadcast(msg: SwToClient) {
2 const clients = await self.clients.matchAll({ includeUncontrolled: true });
3 for (const c of clients) c.postMessage(msg);
4}- 利用
clients.matchAll取得所有視窗分頁。 - 針對每個視窗用
postMessage發送訊息,即可達到廣播。 - 可用於發送更新通知(如
SW_UPDATED)及錯誤通知等。
- 客戶端的處理
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);- 透過從客戶端發送
PING並接收來自Service Worker的回應,您可以驗證雙向通訊是否正常運作。這樣使得測試連線狀態及訊息處理變得更容易。
1navigator.serviceWorker.addEventListener('message', (e) => {
2 const msg = e.data as SwToClient;
3 switch (msg.type) {
4 case 'SW_READY':
5 console.log('Service Worker is ready');
6 // Example: hide loading spinner or enable offline UI
7 break;
8 case 'SW_UPDATED':
9 console.log('A new version of the Service Worker is available');
10 // Example: show update notification or reload prompt
11 const shouldReload = confirm('A new version is available. Reload now?');
12 if (shouldReload) {
13 window.location.reload();
14 }
15 break;
16 case 'CACHE_CLEARED':
17 console.log('Cache cleared');
18 // Example: show confirmation message to user
19 alert('Cache has been successfully cleared.');
20 break;
21 case 'PING':
22 console.log(`Received PING response, ts=${msg.ts}`);
23 break;
24 }
25});- {^ i18n_speak
クライアント側では
Service Workerから送信されるメッセージを受信し、種類に応じて処理を分岐します。SW_READYは初期化完了、SW_UPDATEDは新バージョン検出、CACHE_CLEAREDはキャッシュ削除完了、PINGは通信確認を示します。各メッセージに応じて、UI の更新やリロード、通知表示などを行います。^}
型別化訊息傳遞的優點
- 使用型別訊息可以明確界定可收發哪些訊息,且有自動完成與型別檢查提升安全性。
postMessage實現 一對一 通訊,broadcast則實現 一對多 廣播。- 可輕鬆實現諸如更新通知(
SW_UPDATED)、快取管理(CACHE_CLEARED)、健康檢查(PING)等功能。
總結
- TypeScript 在 Service Worker API 呼叫與訊息傳遞上提供型別安全,大幅提升開發效率與可維護性。
- 理解
install、activate、fetch等生命週期事件,並根據情境選用適合的快取策略(如 cache-first 或 network-first),能提升使用者體驗。 - 在操作上,熟悉快取版本管理及更新流程(
updatefound、waiting、SKIP_WAITING等)是不可或缺的。 - 透過對客戶端與
Service Worker通訊採用嚴格型別的訊息,你可以避免錯誤實作,並建立一個易於長期擴充和維護的系統。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。