
Service Worker 概念與用途
在現代 Web 開發中,我們經常面臨網路連線不穩或使用者離線時的挑戰。Service Worker 應運而生,提供了一種在使用者背景中執行的腳本,它不需直接與網頁互動,卻能攔截網路請求並回傳自行定義的資源。換言之,Service Worker 像是一個介於網頁與伺服器之間的代理層,讓開發者可以掌控資源的快取與提取 developer.mozilla.org 。透過這層代理,我們可以實現離線瀏覽、快速載入,以及推播通知等進階功能,為使用者打造接近原生應用的體驗。
本真案例:想像使用者在搭乘捷運時網路不佳,但您的應用已透過 Service Worker 將關鍵資源快取於本機。即使斷網,用戶仍能看到先前載入的內容,而不是空白錯誤頁。這種「離線優先」的體驗正是 Service Worker 的魅力所在。更棒的是,一旦連回網路,Service Worker 還能靈活地從遠端更新資料,確保下次開啟時內容是新的。總而言之,Service Worker 的核心用途在於提升網站在離線或弱網路環境下的可靠性與速度。
註冊與安裝 Service Worker
了解了概念後,我們來一步步實作 Service Worker。以下將介紹如何註冊 (register) Service Worker、在安裝 (install) 階段進行快取,以及在啟用 (activate) 階段管理快取。假設我們的 Service Worker 檔名為 sw.js,並放置在網站的根目錄下。
註冊 Service Worker:
在網頁的主 JavaScript 檔案中(例如 main.js),使用瀏覽器提供的 API 註冊 Service Worker。註冊時可以指定檔案路徑和作用域 (scope)。若註冊成功,瀏覽器會在背景載入並執行 Service Worker。範例如下:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker 已註冊成功:', registration);
})
.catch(error => {
console.error('Service Worker 註冊失敗:', error);
});
}
上述程式碼先檢查瀏覽器是否支援 Service Worker,然後呼叫 register() 方法。/sw.js 是 Service Worker 檔案的路徑。成功後,可以透過 registration 物件了解 Service Worker 的狀態;如有錯誤則在控制台列出。提示:register() 可接受 { scope: '/some/path/' } 選項來變更控制範圍,但切記 Service Worker 預設只能控制與自身檔案所在目錄相同或以下層級的資源。若將 sw.js 放在子資料夾,務必設定適當的 scope,否則無法攔截根目錄的請求。
安裝階段與預快取資源:
當瀏覽器載入並執行 Service Worker 腳本後,會觸發首次的 install 事件。在這階段通常我們會預先快取應用的核心檔案(例如首頁、CSS、JavaScript、影像資源等),以便之後能離線存取。利用 caches API 可以開啟命名的快取庫,將指定資源加入快取。範例如下,在 sw.js 中監聽安裝事件:
const CACHE_NAME = 'site-static-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/logo.png'
];
self.addEventListener('install', event => {
// 安裝時將核心資源加入快取
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(urlsToCache);
})
);
});
在上述程式中,我們定義了一個快取名稱(site-static-v1)以及一組需要快取的資源清單。event.waitUntil() 確保在安裝事件中,直到指定的快取操作完成後才算安裝成功。如果所有資源順利快取,Service Worker 就能順利安裝並進入下一階段。本源提醒:版本號(如 v1)用於快取版本控制。當您對網站資源做了更新,應提升版本號(例如改為 v2)以讓新的 Service Worker 在安裝時建立新的快取,避免持續使用舊資源。
啟用階段與快取管理:
安裝完成後,若先前已有舊的 Service Worker 正在控制頁面,新版本的 Service Worker 會進入 等待 (waiting) 狀態。當沒有舊版本頁面正在使用時(或在調試時手動跳過等待),會觸發 activate 事件,新 Service Worker 開始控制頁面。通常我們會在此階段進行快取清理,例如刪除舊的快取資料,以釋放空間並避免過期資源繼續被使用。範例如下:
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (!cacheWhitelist.includes(cacheName)) {
// 刪除非白名單裡的舊快取
return caches.delete(cacheName);
}
})
);
})
.then(() => self.clients.claim())
);
});
上述程式使用 caches.keys() 取得所有存在的快取庫名稱,過濾出非當前版本 (CACHE_NAME) 的快取並刪除。最後呼叫 self.clients.claim(),使新 Service Worker 立即接管所有客戶端頁面,而不必等使用者重新載入。至此,我們的 Service Worker 已完成註冊、安裝和啟用流程。接下來,核心的工作是攔截網路請求並套用快取策略來響應。
基本快取策略設計
- Cache First(快取優先)
- Network First(網路優先)
- Cache Only(僅快取)
- Network Only(僅網路)
- Stale-While-Revalidate(陳舊先用,後更新)
每種策略都有其適用場合,以下將逐一說明其原理並提供對應的實作範例程式碼。請注意,這些程式碼片段應寫在 Service Worker 腳本 (sw.js) 中的 fetch 事件監聽器內部,藉此攔截客戶端發出的請求。
Cache First(快取優先)
developer.chrome.com Cache First(快取優先) 策略顧名思義,就是優先從快取取得資源。當收到請求時,Service Worker 先檢查該請求對應的資源是否已存在於快取中:
- 如果快取命中:直接返回快取的資源,省去網路延遲。
- 如果快取沒有:再向網路請求資源,成功取得後將其存入快取,以便下次使用。
這種策略確保已快取的內容能以最快速度提供,特別適合用於不常更動的資源(例如前端框架腳本、字型檔案、圖片等)。即使使用者離線,之前快取的內容仍可提供。然而,需要注意的是,快取優先會導致資源更新延遲——只要快取有資料就不會去抓新版本,除非我們實作額外的更新機制或使用不同快取版本名稱。以下為 Cache First 的基本實作:
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
// 於快取中找到資源,直接回傳
return cachedResponse;
}
// 快取無該資源,透過網路提取
return fetch(event.request).then(networkResponse => {
// 將新的資源加入快取
return caches.open('site-static-v1').then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});
在這段程式碼中,caches.match(event.request) 用於搜尋任何已快取的對應資源。如果找到則立即回傳快取內容;若沒有則使用 fetch 從網路抓取。取得的 networkResponse 我們透過 cache.put 存入快取 (site-static-v1 是先前定義的快取庫),並回傳給使用者。下次相同請求來臨時,就能從快取中快速供應。不難想像,Cache First 非常適合離線優先的情境:例如使用者第二次造訪應用時,大部分靜態資源早已緩存在本地,即使斷網也能正常瀏覽。
Network First(網路優先)
developer.chrome.com 相對於快取優先,「Network First(網路優先)」策略走的是線上優先路線。它先向網路請求最新的資源,以確保資料最新鮮,但同時備援一份離線快取以防網路不可用:
- 在線狀態:嘗試透過 fetch 從網路取得資源。如果成功,將響應內容同時保存到快取,供未來離線時使用。
- 離線或網路請求失敗:則退而求其次,從快取中尋找最後一次存有的該資源並返回。
這種策略適合即時性高的內容,例如 API 資料或頻繁更新的頁面(如動態新聞、用戶帖子)。它保證有網路時用戶總是看到最新資料,而在無網路時至少能看到最近一次的緩存內容,不至於完全無法使用。Network First 策略的實作範例如下:
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(networkResponse => {
// 網路請求成功的話,更新快取
return caches.open('site-static-v1').then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
}).catch(() => {
// 如果網路不可用,從快取取最後的資源
return caches.match(event.request);
})
);
});
在這段程式中,我們首先執行 fetch(event.request) 嘗試抓取網路資源。如果拿到回應,接著開啟快取並 cache.put 更新緩存(確保下次離線時能取到最新版本)。respondWith 會先返回這個網路回應給使用者。萬一網路請求失敗(例如離線狀態),Promise 會被拒絕而進入 .catch,此時我們呼叫 caches.match(event.request) 來從已有的快取中尋找資源作為後備。本源提示:務必確保先前有將對應資源存入快取(可能是先前的線上請求或安裝階段預快取),否則離線時 caches.match 也會拿不到內容。總的來說,Network First 提供了一種折衷:在網路順暢時用最新資料,網路不行時至少有舊資料可用。
Cache Only(僅快取)
developer.chrome.com 有些場景下,我們希望完全依賴快取來提供內容,甚至不嘗試網路請求——這就是 Cache Only(僅快取) 策略。Service Worker 收到請求時只在快取中查找資源,如有即返回,沒有也不會再去抓取網路資源。在離線應用或固定內容的情境中,Cache Only 可以確保應用只使用事先緩存的內容運作:
self.addEventListener('fetch', event => {
// 直接從快取查詢並回應(不發出網路請求)
event.respondWith(caches.match(event.request));
});
這是最簡潔的實現:一行 respondWith(caches.match()) 完成響應。只有當 event.request 事先已被快取(例如在安裝階段用 cache.addAll 預載)時,使用者才能順利取得內容。否則,若快取中找不到資源,請求將回傳 undefined 而導致失敗。因此 Cache Only 一般僅適用在明確可以預期資源都已離線準備好的情況,比如一個完全離線運行的文件瀏覽器應用,或是在安裝 Service Worker 時就將所有必要資源都打包快取的單頁應用。這種策略保障了應用即使斷網也不會嘗試連線(節省流量與電量),但代價是一旦快取遺漏或過期,資源將無法取得。
Network Only(僅網路)
developer.chrome.com 與僅快取相反,Network Only(僅網路) 策略完全不使用快取,每次請求都直接訪問網路。Service Worker 在這種策略下其實形同虛設,只是單純地將請求轉發出去,不做任何攔截處理。這聽起來像是「不策略」的策略,但在某些情況非常有用:例如處理即時更新且絕對不能使用過時資料的 API 請求、或者需要確保用戶每次操作都直達服務端的功能。
Network Only 非常容易實現:在 fetch 事件中不調用 event.respondWith(或讓函式直接 return),瀏覽器就會自行走原本的網路流程;如果要明確表達,我們可以寫一個空的響應處理,如下:
self.addEventListener('fetch', event => {
// 不攔截,直接交給瀏覽器的默認網路處理
// (等同於 event.respondWith(fetch(event.request)))
});
上例中,我們沒有對請求做任何特殊處理,相當於所有請求都繞過 Service Worker 快取,直接去網路抓取最新資料。使用此策略時,要承擔使用者離線或伺服器無回應時無法提供內容的風險。因此,Network Only 常用在對即時性要求極高且不可離線的功能上,如信用卡付款流程、需要強一致性的數據查詢等。在一般情況下,我們較少為靜態資源使用純網路策略,但有時會為了避免干擾而讓某些特定接口不被 Service Worker 快取(比如後台管理介面的 API),這時就可以對那些路由採用 Network Only。總而言之,僅網路策略保證了每次都從源頭取得最新資料,但失去快取帶來的速度與離線優勢。
Stale-While-Revalidate(先陳舊後驗證)
developer.chrome.com Stale-While-Revalidate(陳舊先用,後更新) 策略可以說是結合了前幾種方法的優點,略具複雜但非常實用。它的理念是:先快速返回快取中的舊資源(stale),同時在背景發出網路請求以取得最新資源並更新快取(revalidate)。這樣用戶立即獲得回應的同時,我們悄悄確保下次請求時快取已是最新內容。其運作步驟如下:
- 第一步(無快取時):資源尚未被快取,跟 Network First 類似,從網路抓取並緩存,返回最新內容。
- 隨後的請求:發現快取中已有舊的響應,立即返回它讓使用者不中斷。然而同時(Background)向網路取新內容。
- 取得新內容後:更新快取中該資源的快取版本。但本次請求用戶已拿到舊內容,不會等待新內容。等到下一次相同請求時,用戶將拿到更新後的資源。
這種「先用舊的頂著,背後更新新的」策略非常適合對即時性要求不那麼嚴苛但又希望最終一致的資源,例如個人頭像、文章列表等——即使不是最新的也無傷大雅,但還是希望有機會更新。下面是 Stale-While-Revalidate 的實現範例:
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('site-static-v1').then(cache => {
return cache.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
// 獲取最新資源後更新快取
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// 快取有則立即回應,沒有則等待網路
return cachedResponse || fetchPromise;
});
})
);
});
以上程式碼中,我們在收到請求時打開特定的快取,先用 cache.match 檢查是否有舊響應。然後不管有無,我們都啟動一個 fetchPromise 去網路抓取新資源。接著 respondWith 的重點是回傳 cachedResponse || fetchPromise:也就是優先返回快取內容(若存在),同時瀏覽器仍在背景執行 fetchPromise。如果快取沒有舊內容,那就直接等網路回應。等網路拿到後,我們使用 cache.put 更新快取。如此一來,用戶在有舊內容時能秒開頁面,而更新的資源悄悄存好,下次就能即時提供最新版本。Stale-While-Revalidate 策略在實務中極為常見,因為它在用戶體驗和資料新鮮度間取得了絕妙的平衡:用戶幾乎無感延遲,又能確保長期使用下內容不會一成不變。
Service Worker 部署與測試
完成 Service Worker 的編寫後,最後一哩路是將它部署到正確的環境並進行測試與調試。由於 Service Worker 涉及瀏覽器背景執行與快取行為,我們需要特別注意以下事項,以確保部署順利且功能如預期:
- 本機開發與 HTTPS:正如前面提及,Service Worker 要求網站透過 HTTPS 提供。在本機開發時,可以使用 localhost(被視為安全來源)進行測試。如果您只是開啟檔案(file://)模式,Service Worker 將無法註冊。建議使用簡易的本地開發伺服器,例如 Node.js 的 serve 套件或 Python 簡易HTTP伺服器 (python -m http.server),將專案架站在 http://localhost:PORT 下測試。確認瀏覽器 Console 中無報錯訊息,且 navigator.serviceWorker.controller 狀態正常。
- 首次載入與快取填充:部署後,用線上狀態載入網頁,讓 Service Worker 完成安裝並快取必要資源。您可以在瀏覽器的開發者工具中,透過 Application (或 應用程式) 面板的 Service Workers 頁籤查看註冊情況。正常的話,應能看到新註冊的 Service Worker 處於 Active 或 Activated 狀態。此時建議嘗試關閉網路或切換到離線模式,然後重新載入網頁,確認快取策略是否發揮作用——離線時原本應快取的內容是否順利顯示,是否有資源取自快取而非網路。
- Chrome DevTools 調試:Chrome 等瀏覽器提供了豐富的工具來調試 Service Worker。在 Application 面板 > Service Workers 頁面,您可以:
- 勾選 "Offline" 或在 Network 面板將網路狀態設為 Offline,以模擬無網環境下的行為。
- 勾選 "Update on reload",讓瀏覽器每次載入頁面都自動尋找並套用更新的 Service Worker(方便開發時測試更新機制)。
- 查看 Service Worker 列表中的版本狀態:如 installing、waiting、active 等。如果有新的 Service Worker 在 waiting,可按 Skip Waiting 讓其立即接管。
- 在 Cache Storage 區域檢視快取內容。您可以展開特定的 Cache 名稱(例如我們定義的 site-static-v1),檢查裡面列出的資源清單,確認是否與預期相符。
- 使用 Console 日誌來除錯:在 Service Worker 腳本中加上 console.log 語句,透過 Application > Service Workers 選單中的 Inspect 或 檢視 按鈕打開 Service Worker 的專屬除錯視窗,能看到那些 console 輸出。此外也可以在 Sources 面板中找到 sw.js 並下斷點進行偵錯。
- 部署更新與版本控制:當您對 Service Worker 或快取資源做了修改,部署新版時要注意舊的快取可能造成的影響。由於瀏覽器對已安裝的 Service Worker 採用惰性更新(通常每隔24小時或使用者重載時檢查),若新版本的 sw.js 檔案內容不變,瀏覽器可能不會重新載入它。因此務必在發佈更新時修改 Service Worker 檔案的某處(哪怕只是版本號註解),以確保瀏覽器察覺變化並進行更新流程。新 Service Worker 安裝後,由於預設採「等待中」策略,舊版仍掌控當前頁面,導致新的快取內容可能未被使用。您可以提示用戶關閉所有頁籤或透過程式主動調用 self.skipWaiting() 和 clients.claim() 來加速新舊交替。快取部分,正如前述,我們通常使用版本化的 CACHE_NAME 並在 activate 階段刪除陳舊快取,以避免新版網站仍讀取到舊版快取的資源。
總而言之,部署與測試階段要仔細驗證 Service Worker 的生命週期(安裝->等待->啟用)是否正常循環,快取策略是否按預期執行,以及在各種網路條件下(線上、離線、慢速)應用都能提供良好的用戶體驗。多善用瀏覽器提供的開發者工具,可以讓您對 Service Worker 的運作狀況了若指掌。
常見問答(FAQ)
採用 Stale-While-Revalidate:如前所述,它能在背景更新快取,確保用戶多次訪問間隔期間快取能獲得刷新。
快取版本與過期:對於 Cache First 策略,請務必隨版本變更快取名稱,讓新的 Service Worker 安裝時自動棄用舊快取。此外,您也可以在 Fetch 處理時加上資源的版本查詢參數(例如 file.js?v=123),一旦版本變動就視為不同資源以重新抓取。
使用 Cache-Control/ETag 機制:雖然 Service Worker 可以完全接管快取,但您仍可結合伺服器的 HTTP 標頭來判斷資源是否更新。例如,在 Network First 策略中,即便網路可用,也可以先 fetch 後檢查 response.headers 的 Last-Modified 或 ETag,決定是否要替換本地快取。
主動推送更新通知:進階一點,可以利用 Service Worker 的推播通知或主動同步 (background sync) 來告知用戶有新版本,讓他們同意或自動刷新。
最重要的是制定符合應用需求的快取期限。例如快速變動的 API 資料可選擇 Network First 或極短暫的快取,穩定的靜態資源則可 Cache First 並長時間快取。透過策略組合,達到既有速度又有新鮮度的平衡。
self.addEventListener('fetch', event => {
const req = event.request;
const url = new URL(req.url);
if (req.mode === 'navigate') {
// 網頁導航請求 (HTML) -> 網路優先
event.respondWith(/* network-first handler */);
} else if (url.pathname.startsWith('/api/')) {
// API 請求 -> 網路優先或僅網路
event.respondWith(/* network-only or network-first handler */);
} else if (req.destination === 'image') {
// 圖片資源 -> 快取優先或陳舊後更新
event.respondWith(/* cache-first or stale-while-revalidate handler */);
} else {
// 其他靜態資源(CSS/JS) -> 快取優先
event.respondWith(/* cache-first handler */);
}
});
如上,根據請求類型選用不同策略,可以兼顧各類資源的特性,提升整體效能與使用者體驗。實務上也可以使用像 Workbox 這類的高階函式庫更方便地定義多種路由規則與策略。不論如何,靈活運用多種快取策略是 PWA 優化的關鍵,也是 Service Worker 作為網路請求「織心者」的重要價值所在。