從 PWA 認識 Service Worker#
Service Worker 是漸進式網頁應用程式(PWA, Progressive Web App)的關鍵元件之一。PWA 涵蓋的範圍極廣,本章不打算重講整套,焦點放在 Service Worker 提供的快取能力。
嚴格來說 Service Worker 是 Web Worker 的一種。日常寫的 JavaScript 跑在 main thread(UI thread)上,Service Worker 則跑在獨立的執行緒上:
- 不會被畫面渲染或運算所阻塞。
- 能在瀏覽器關閉的情況下繼續在背景運作。
- 它是位於瀏覽器與網路層之間的代理(proxy),透過監聽
fetch事件可以攔截使用者發出的請求。 - 提供 Cache API,可以決定要不要回傳快取內容。
在快取的優先順序上,Service Worker Cache 排在 HTTP 快取(HTTP Caching)之前。如果撇開各家瀏覽器自行實作的 memory cache 不算,Service Worker Cache 就是開發者可掌控範圍中的第一道防線。
Service Worker 讓網頁可以做到許多以前難以實現的事,例如離線瀏覽、推播通知、Background Sync 等。
生命週期概念#
Service Worker 擁有完全獨立於頁面的生命週期,且開發者對它的控制權非常細。需要特別記得幾件事:它沒有辦法直接操作 DOM,與頁面的訊息傳遞要透過 postMessage。
要使用 Service Worker 必須符合幾個條件:
- 網站必須使用 HTTPS(或 localhost)。
- 必須先註冊 Service Worker 檔案。
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./sw.js", { scope: "/ironman" })
.then((reg) => {
console.log("Registration succeeded. Scope is " + reg.scope);
})
.catch((error) => {
console.log("Registration failed with " + error);
});
}scope 決定 Service Worker 作用的範圍。沒有指定時預設為根目錄;上例則是 /ironman 之下都歸這個 Service Worker 管。
控制 Service Worker 的方式是事件監聽,最重要的有 install、activate、message、fetch。
install:註冊靜態資源#
註冊成功後,瀏覽器會在背景啟動一個 Service Worker,這時觸發 install 事件。一般會在這個階段把離線需要的靜態資源預先放進快取,全部成功才會進入下一步;只要有一個失敗,整個安裝過程就視為失敗,並等待下次重新嘗試。
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("v1").then((cache) => {
return cache.addAll(["/ironman.js", "/ironman.css"]);
})
);
});操作快取使用的是 Cache API,是 Service Worker 與快取互動的標準介面。
activate:接管頁面#
完成安裝後 Service Worker 會進入 activate 階段,並接管 scope 範圍內的頁面。這個階段適合做清理工作,例如把不再使用的舊版本快取刪除。
self.addEventListener("activate", (event) => {
// 例如清掉舊版的 cache
});當頁面沒有在使用時,Service Worker 會被 terminated 以節省記憶體;下次需要時再被喚醒。
fetch:攔截請求#
fetch 事件是 Service Worker 在快取扮演要角的關鍵。最基本的用法是:先看快取裡有沒有,沒有再走網路。
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});從這個 API 設計就能看出 Service Worker 的彈性:請求被攔截下來之後,要回傳什麼內容、要不要存進快取、要不要降級,全部由開發者決定。
為什麼還需要 Service Worker Cache#
HTTP 快取(HTTP Caching)已經很實用了,為什麼還要再學 Service Worker 的快取機制?主要兩個原因:
- 更細粒度的控制:HTTP Caching 受限於 Header 的語法,能做的事情相對有限。Service Worker 是用程式碼控制,幾乎想得到的策略都能實作。
- 離線優先(Offline First):HTTP Caching 並不是為了離線而設計,且只有「拜訪過的頁面」才有快取。Service Worker 可以主動將需要的資源存進快取,配合各種策略實現完整的離線體驗。即使瀏覽器有 HTTP 快取,部分情境下也不一定會走快取,行為會因實作而異。
常見的 Cache Strategies#
實作離線體驗時,有幾種典型的策略:
cache onlynetwork onlycache falling back to networknetwork falling back to cachecache then network(也就是 stale-while-revalidate)
cache only#
無論如何只回傳快取版本。實務上很少單獨使用,比較適合與 codebase 一起 build 出來的靜態資源。
self.addEventListener("fetch", (event) => {
event.respondWith(caches.match(event.request));
});network only#
完全不查快取直接發請求。常見場景是非 GET 的請求,本來就不適合被快取。
self.addEventListener("fetch", (event) => {
event.respondWith(fetch(event.request));
});cache falling back to network#
最常見的模式:先查快取,沒有才打網路。是想實作離線優先(Offline First)時的好起點。
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});network falling back to cache#
對變動頻率高的內容(例如文章列表、排行榜)較合適:先以網路為主拿到最新資料,失敗時回退到快取。缺點是當網路非常緩慢卻沒完全斷線時,使用者會等很久。
self.addEventListener("fetch", (event) => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});cache then network(stale-while-revalidate)#
先回傳快取版本讓使用者立即看到內容,同時在背景發出請求並更新快取,下次就能看到較新的資料。這種策略在使用者體驗與資料新鮮度間做了平衡。
self.addEventListener("fetch", (event) => {
const cached = caches.match(event.request);
const fetched = fetch(event.request);
const fetchedCopy = fetched.then((resp) => resp.clone());
event.respondWith(
Promise.race([fetched.catch(() => cached), cached])
.then((resp) => resp || fetched)
.catch(() => new Response(null, { status: 404 }))
);
event.waitUntil(
Promise.all([fetchedCopy, caches.open("cache-v1")])
.then(([response, cache]) => cache.put(event.request, response))
.catch(() => {})
);
});與 HTTP Cache 一起搭配#
Service Worker Cache 與 HTTP 快取(HTTP Caching)的目標其實不完全相同,所以怎麼搭配也是門學問。如果兩者的過期時間設成一樣,HTTP Cache 幾乎沒有意義;建議讓兩者過期時間錯開,並選擇互補的快取策略,這樣才會像「雙層防護網」一樣發揮作用。
Google 對於兩層快取怎麼搭配有寫過一篇詳細文章,有需要時可以延伸閱讀。
Workbox#
直接手寫 Service Worker 通常會出現大量 boilerplate code。Google 推出的 Workbox 把常用模式封裝起來,多數情境只要幾行設定就能搞定。除非有非常客製化的需求,做 PWA 或想用上 Service Worker 的能力時,建議優先考慮 Workbox。
本章重點#
- Service Worker 是跑在獨立執行緒、能攔截網路請求的代理層,是漸進式網頁應用程式(PWA, Progressive Web App)的核心之一。
- 它的快取優先順序高於 HTTP 快取(HTTP Caching),是開發者第一道可控的快取。
- 透過
install、activate、fetch等生命週期事件,可以決定快取什麼、何時清理、請求如何回應。 - 常見策略包括 cache only、network only、cache falling back to network、network falling back to cache,以及 stale-while-revalidate。
- Service Worker Cache 與 HTTP Cache 應錯開過期時間並搭配不同策略,才能發揮兩層保護的價值。
- 真實專案推薦直接用 Workbox,省下手寫一堆樣板程式碼的成本。