js13kGames:使用 Service Worker 讓 PWA 實現離線工作
既然我們已經瞭解了 js13kPWA 的結構,並且看到了基本框架的執行,現在我們來看看如何使用 Service Worker 實現離線功能。在本文中,我們將探討它如何在我們的 js13kPWA 示例(也可以檢視原始碼)中使用。我們將研究如何新增離線功能。
Service Worker 詳解
Service Worker 是瀏覽器和網路之間的虛擬代理。它們使得正確快取網站資源並使其在使用者裝置離線時可用成為可能。
它們在與我們頁面主 JavaScript 程式碼不同的執行緒上執行,並且無法訪問 DOM 結構。這引入了一種與傳統 Web 程式設計不同的方法——API 是非阻塞的,並且可以在不同上下文之間傳送和接收通訊。您可以使用基於 Promise 的方法,讓 Service Worker 處理一些工作,並在結果準備好後接收它。
Service Worker 不僅能提供離線功能,還可以處理通知或執行復雜的計算。Service Worker 非常強大,因為它們可以控制網路請求,修改它們,提供從快取中檢索的自定義響應,或者完全合成響應。
要了解更多關於 Service Worker 的資訊,請參閱 離線和後臺操作。
js13kPWA 應用中的 Service Worker
讓我們來看看 js13kPWA 應用如何使用 Service Worker 來提供離線功能。
註冊 Service Worker
我們將從檢視在 app.js 檔案中註冊新 Service Worker 的程式碼開始。
let swRegistration = null;
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./pwa-examples/js13kpwa/sw.js")
.then((reg) => {
swRegistration = reg;
});
}
如果瀏覽器支援 Service Worker API,則使用 ServiceWorkerContainer.register() 方法將其註冊到網站。其內容位於 sw.js 檔案中,並在註冊成功後執行。這是 app.js 檔案中唯一包含 Service Worker 程式碼的部分;所有其他特定於 Service Worker 的程式碼都寫在 sw.js 檔案本身中。
Service Worker 的生命週期
註冊完成後,sw.js 檔案會自動下載、安裝,然後啟用。
安裝
該 API 允許我們為感興趣的關鍵事件新增事件監聽器——第一個是 install 事件。
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
});
在 install 監聽器中,我們可以初始化快取並將檔案新增到其中以供離線使用。我們的 js13kPWA 應用正是這樣做的。
首先,建立一個用於儲存快取名稱的變數,並將應用外殼檔案列在一個數組中。
const cacheName = "js13kPWA-v1";
const appShellFiles = [
"/pwa-examples/js13kpwa/",
"/pwa-examples/js13kpwa/index.html",
"/pwa-examples/js13kpwa/app.js",
"/pwa-examples/js13kpwa/style.css",
"/pwa-examples/js13kpwa/fonts/graduate.eot",
"/pwa-examples/js13kpwa/fonts/graduate.ttf",
"/pwa-examples/js13kpwa/fonts/graduate.woff",
"/pwa-examples/js13kpwa/favicon.ico",
"/pwa-examples/js13kpwa/img/js13kgames.png",
"/pwa-examples/js13kpwa/img/bg.png",
"/pwa-examples/js13kpwa/icons/icon-32.png",
"/pwa-examples/js13kpwa/icons/icon-64.png",
"/pwa-examples/js13kpwa/icons/icon-96.png",
"/pwa-examples/js13kpwa/icons/icon-128.png",
"/pwa-examples/js13kpwa/icons/icon-168.png",
"/pwa-examples/js13kpwa/icons/icon-192.png",
"/pwa-examples/js13kpwa/icons/icon-256.png",
"/pwa-examples/js13kpwa/icons/icon-512.png",
];
接下來,在第二個陣列中生成要與 data/games.js 檔案中的內容一起載入的圖片的連結。然後,使用 Array.prototype.concat() 函式將這兩個數組合並。
const gamesImages = [];
for (const game of games) {
gamesImages.push(`data/img/${game.slug}.jpg`);
}
const contentToCache = appShellFiles.concat(gamesImages);
然後我們可以管理 install 事件本身。
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
console.log("[Service Worker] Caching all: app shell and content");
await cache.addAll(contentToCache);
})(),
);
});
這裡有兩點需要解釋:ExtendableEvent.waitUntil 的作用,以及 caches 物件是什麼。
Service Worker 不會安裝,直到 waitUntil 中的程式碼執行完畢。它返回一個 Promise——這種方法是必需的,因為安裝可能需要一些時間,所以我們必須等待它完成。
caches 是在給定 Service Worker 的作用域中可用的特殊 CacheStorage 物件,用於儲存資料——儲存到 Web Storage 將不起作用,因為 Web Storage 是同步的。使用 Service Worker,我們改用 Cache API。
在這裡,我們用給定的名稱開啟一個快取,然後將應用使用的所有檔案新增到快取中,以便下次載入時可用。資源透過其請求 URL 標識,該 URL 相對於 Worker 的 位置。
您可能會注意到我們沒有快取 game.js。這是包含我們在顯示遊戲時使用的資料的檔案。實際上,這些資料很可能來自 API 端點或資料庫,快取資料意味著在有網路連線時定期更新它。我們在此處不深入探討,但 Periodic Background Sync API 是關於此主題的進一步閱讀的好材料。
啟用
還有一個 activate 事件,其用法與 install 相同。此事件通常用於刪除任何不再需要的檔案並清理應用。我們在應用中不需要這樣做,所以我們跳過它。
響應請求
我們還有一個可用的 fetch 事件,它會在我們的應用發出每個 HTTP 請求時觸發。這非常有用,因為它允許我們攔截請求並用自定義響應來響應它們。例如:
self.addEventListener("fetch", (e) => {
console.log(`[Service Worker] Fetched resource ${e.request.url}`);
});
響應可以是任何我們想要的東西:請求的檔案、其快取的副本,或者一段將執行特定操作的 JavaScript 程式碼——可能性是無限的。
在我們的示例應用中,只要資源實際存在於快取中,我們就從快取而不是網路提供內容。無論應用是線上還是離線,我們都這樣做。如果檔案不在快取中,應用會先將其新增進去,然後再提供。
self.addEventListener("fetch", (e) => {
e.respondWith(
(async () => {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
if (r) {
return r;
}
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, response.clone());
return response;
})(),
);
});
在這裡,我們用一個函式響應 fetch 事件,該函式嘗試在快取中查詢資源並返回響應(如果存在)。如果不存在,我們使用另一個 fetch 請求從網路獲取它,然後將響應儲存在快取中,以便下次請求時可用。
FetchEvent.respondWith 方法接管了控制——這部分充當了應用和網路之間的代理伺服器。這使我們能夠用任何我們想要的響應來響應每一個請求:由 Service Worker 準備,從快取中獲取,如果需要則進行修改。
就這樣!我們的應用在安裝時快取其資源,並透過 fetch 從快取中提供它們,因此即使在使用者離線時也能工作。它還在新增新內容時快取新內容。
更新
還有一點需要說明:當有新版本應用包含新資產可用時,如何升級 Service Worker?快取名稱中的版本號是關鍵。
const cacheName = "js13kPWA-v1";
當這更新到 v2 時,我們可以將所有檔案(包括我們的新檔案)新增到新的快取中。
contentToCache.push("/pwa-examples/js13kpwa/icons/icon-32.png");
// …
self.addEventListener("install", (e) => {
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
await cache.addAll(contentToCache);
})(),
);
});
一個新的 Service Worker 在後臺安裝,前一個 (v1) 正確執行,直到沒有頁面在使用它——然後新的 Service Worker 被啟用,並從舊的 Service Worker 接管頁面的管理。
清除快取
還記得我們跳過的 activate 事件嗎?它可以用來清除我們不再需要的舊快取。
self.addEventListener("activate", (e) => {
e.waitUntil(
caches.keys().then((keyList) =>
Promise.all(
keyList.map((key) => {
if (key === cacheName) {
return undefined;
}
return caches.delete(key);
}),
),
),
);
});
這確保了我們快取中只有我們需要的檔案,因此我們不會留下任何垃圾;瀏覽器中的可用快取空間是有限的,所以清理自己是一個好主意。
其他用例
從快取提供檔案並不是 Service Worker 提供的唯一功能。如果您有複雜的計算需要執行,您可以將其從主執行緒解除安裝並在 Worker 中執行,並在結果可用時立即接收它們。從效能角度來看,您可以預取當前不需要但將來可能需要的資源,這樣當您實際需要這些資源時,應用程式會更快。
總結
在本文中,我們簡單地探討了如何使用 Service Worker 讓您的 PWA 實現離線工作。如果您想了解更多關於 Service Worker API 背後的概念以及如何更詳細地使用它,請務必查閱我們的進一步文件。
在處理 推送通知時也會使用 Service Worker——這將在後續文章中進行解釋。