CycleTracker:Service Worker
到目前為止,我們已經為 CycleTracker 編寫了 HTML、CSS 和 JavaScript。我們添加了一個清單檔案,其中定義了顏色、圖示、URL 和其他應用功能。我們已經有了一個可用的 PWA!但它還不能離線工作。在本節中,我們將編寫所需的 JavaScript,將我們功能完備的 Web 應用轉換為一個可以作為獨立應用分發並能無縫離線工作的 PWA。
如果您尚未完成,請複製 HTML、CSS、JavaScript 和 清單 JSON 檔案。將它們分別儲存為 index.html、style.css、app.js 和 cycletracker.json。
在本節中,我們將建立 sw.js,即 Service Worker 指令碼,它將把我們的 Web 應用轉換為 PWA。我們已經有一個 JavaScript 檔案;HTML 檔案的最後一行呼叫了 app.js。這個 JavaScript 提供了標準 Web 應用功能的所有功能。我們不會像使用 <script> 的 src 屬性呼叫 app.js 檔案那樣呼叫 sw.js 檔案,而是透過註冊 Service Worker 在 Web 應用及其 Service Worker 之間建立關係。
在本課程結束時,您將擁有一個功能完備的 PWA;一個漸進增強的 Web 應用,可以完全安裝,甚至在使用者離線時也能正常工作。
Service Worker 的職責
Service Worker 是使應用離線工作同時確保應用始終保持最新狀態的關鍵。為了實現這一點,Service Worker 應包括以下內容:
- 版本號(或其他識別符號)。
- 要快取的資源列表。
- 快取版本名稱。
Service Worker 還負責:
- 在安裝應用時安裝快取。
- 根據需要更新自身和其他應用檔案。
- 刪除不再使用的快取檔案。
我們透過響應三個 Service Worker 事件來完成這些任務,包括:
版本號
一旦 PWA 安裝在使用者的機器上,唯一能通知瀏覽器有更新檔案需要檢索的方法是 Service Worker 發生變化。如果對任何其他 PWA 資源進行了更改 — 如果 HTML 更新了,CSS 中修復了 bug,app.js 中添加了函式,影像被壓縮以減小檔案大小等等 — 已安裝 PWA 的 Service Worker 將不知道它需要下載更新的資源。只有當 Service Worker 以任何方式被修改時,PWA 才會知道可能需要更新快取;這是 Service Worker 的任務。
雖然從技術上講,更改任何字元可能就足夠了,但 PWA 的最佳實踐是建立一個版本號常量,並按順序更新該常量以指示檔案更新。更新版本號(或日期)可以為 Service Worker 提供正式的編輯,即使 Service Worker 本身沒有更改其他任何內容,併為開發人員提供了一種識別應用版本的方法。
任務
透過包含版本號來啟動 JavaScript 檔案
const VERSION = "v1";
將檔案儲存為 sw.js
離線資源列表
為了獲得良好的離線體驗,快取檔案列表應包含 PWA 離線體驗中使用的所有資源。雖然清單檔案可能列出了各種大小的眾多圖示,但應用快取只需要包含應用在離線模式下使用的資產。
const APP_STATIC_RESOURCES = [
"/",
"/index.html",
"/style.css",
"/app.js",
"/icon-512x512.png",
];
您無需在列表中包含所有不同作業系統和裝置使用的各種圖示。但請務必包含應用中使用的任何影像,包括可能在應用載入緩慢時可見的啟動頁面中使用的資產,或在任何“您需要連線網際網路才能獲得完整體驗”型別的頁面中使用的資產。
不要在要快取的資源列表中包含 Service Worker 檔案。
任務
將 CycleTracker PWA 要快取的資源列表新增到 sw.js。
解決方案示例
我們包含了本教程其他部分建立的 CycleTracker 離線執行時所需的靜態資源。我們當前的 sw.js 檔案是:
const VERSION = "v1";
const APP_STATIC_RESOURCES = [
"/",
"/index.html",
"/style.css",
"/app.js",
"/cycletracker.json",
"/icons/wheel.svg",
];
我們包含了 wheel.svg 圖示,儘管我們當前的應用沒有使用它,以防您正在增強 PWA UI,例如在沒有周期資料時顯示徽標。
應用快取名稱
我們有一個版本號和需要快取的檔案。在快取檔案之前,我們需要建立一個快取名稱,用於儲存應用的靜態資源。這個快取名稱應該進行版本控制,以確保在應用更新時,將建立一個新快取並刪除舊快取。
任務
使用 VERSION 號來建立一個帶版本號的 CACHE_NAME,並將其作為常量新增到 sw.js。
解決方案示例
我們將快取命名為 period-tracker-,並附加當前的 VERSION。由於常量宣告在一行上,為了更好的可讀性,我們將其放在資源常量陣列之前。
const VERSION = "v1";
const CACHE_NAME = `period-tracker-${VERSION}`;
const APP_STATIC_RESOURCES = [
// …
];
我們已成功聲明瞭常量;一個唯一的識別符號,作為陣列的離線資源列表,以及每次識別符號更新時都會更改的應用快取名稱。現在讓我們專注於安裝、更新和刪除未使用的快取資源。
PWA 安裝時儲存快取
當用戶安裝或僅僅訪問帶有 Service Worker 的網站時,Service Worker 作用域中會觸發一個 install 事件。我們希望監聽此事件,在安裝時用 PWA 的靜態資源填充快取。每次 Service Worker 版本更新時,瀏覽器都會安裝新的 Service Worker,併發生安裝事件。
install 事件在應用首次使用時發生,或者當瀏覽器檢測到新版本的 Service Worker 時發生。當舊的 Service Worker 被新的替換時,舊的 Service Worker 將作為 PWA 的 Service Worker 使用,直到新的 Service Worker 被啟用。
僅在安全上下文中可用,WorkerGlobalScope.caches 屬性返回一個與當前上下文關聯的 CacheStorage 物件。CacheStorage.open() 方法返回一個 Promise,該 Promise 解析為與作為引數傳入的快取名稱匹配的 Cache 物件。
Cache.addAll() 方法將一個 URL 陣列作為引數,檢索它們,然後將響應新增到給定的快取中。ExtendableEvent.waitUntil() 方法告訴瀏覽器工作正在進行,直到 Promise 解決,並且如果它希望該工作完成,則不應終止 Service Worker。雖然瀏覽器負責在必要時執行和終止 Service Worker,但 waitUntil 方法是向瀏覽器發出的請求,要求在執行任務時不要終止 Service Worker。
self.addEventListener("install", (e) => {
e.waitUntil(
(async () => {
const cache = await caches.open("cacheName_identifier");
cache.addAll(["/", "/index.html", "/style.css", "/app.js"]);
})(),
);
});
任務
新增一個 install 事件監聽器,該監聽器將 APP_STATIC_RESOURCES 中列出的檔案檢索並存儲到名為 CACHE_NAME 的快取中。
解決方案示例
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
cache.addAll(APP_STATIC_RESOURCES);
})(),
);
});
更新 PWA 並刪除舊快取
如前所述,當現有 Service Worker 被新的 Service Worker 替換時,現有 Service Worker 將作為 PWA 的 Service Worker 使用,直到新的 Service Worker 啟用。我們使用 activate 事件刪除舊快取以避免空間不足。我們遍歷命名的 Cache 物件,刪除除了當前快取之外的所有快取,然後將 Service Worker 設定為 PWA 的 controller。
我們監聽當前 Service Worker 的全域性作用域 activate 事件。
我們獲取現有命名快取的名稱。我們使用 CacheStorage.keys() 方法(再次透過 WorkerGlobalScope.caches 屬性訪問 CacheStorage),該方法返回一個 Promise,該 Promise 解析為一個數組,其中包含所有命名 Cache 物件的字串,按其建立順序排列。
我們使用 Promise.all() 方法遍歷命名快取 Promise 列表。all() 方法將可迭代 Promise 列表作為輸入,並返回一個單一的 Promise。對於命名快取列表中的每個名稱,檢查快取是否是當前活動的快取。如果不是,則使用 Cache 的 delete() 方法將其刪除。
最後一行,await clients.claim() 使用 Clients 介面的 claim() 方法,使我們的 Service Worker 能夠將自身設定為我們客戶端的控制器;“客戶端”指的是 PWA 的執行例項。claim() 方法使 Service Worker 能夠“宣告控制”其作用域內的所有客戶端。這樣,在同一作用域內載入的客戶端無需重新載入。
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names.map((name) => {
if (name !== CACHE_NAME) {
return caches.delete(name);
}
return undefined;
}),
);
await clients.claim();
})(),
);
});
任務
將上述 activate 事件監聽器新增到您的 sw.js 檔案中。
fetch 事件
我們可以利用 fetch 事件,以防止已安裝的 PWA 在使用者線上時發出請求。監聽 fetch 事件使得攔截所有請求並用快取響應代替網路請求成為可能。大多數應用程式不需要這種行為。事實上,許多商業模式希望使用者定期發出伺服器請求以進行跟蹤和營銷。因此,儘管攔截請求可能對某些人來說是一種反模式,但為了提高我們的 CycleTracker 應用程式的隱私性,我們不希望應用程式發出不必要的伺服器請求。
由於我們的 PWA 由一個單一頁面組成,對於頁面導航請求,我們返回 index.html 主頁。沒有其他頁面,我們也不希望訪問伺服器。如果 Fetch API 的 Request 只讀 mode 屬性是 navigate,這意味著它正在尋找網頁,我們使用 FetchEvent 的 respondWith() 方法來阻止瀏覽器的預設 fetch 處理,透過使用 caches.match() 方法提供我們自己的響應 Promise。
對於所有其他請求模式,我們像 安裝事件響應 中那樣開啟快取,而是將事件請求傳遞給相同的 match() 方法。它檢查請求是否是儲存的 Response 的鍵。如果是,它返回快取的響應。如果不是,我們返回 404 狀態 作為響應。
使用 Response() 建構函式傳遞一個 null 主體和 status: 404 作為選項,並不意味著我們的 PWA 中存在錯誤。相反,我們需要的一切都應該已經在快取中,如果不在,我們也不會去伺服器解決這個非問題。
self.addEventListener("fetch", (event) => {
// when seeking an HTML page
if (event.request.mode === "navigate") {
// Return to the index.html page
event.respondWith(caches.match("/"));
return;
}
// For every other request type
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(event.request.url);
if (cachedResponse) {
// Return the cached response if it's available.
return cachedResponse;
}
// Respond with a HTTP 404 response status.
return new Response(null, { status: 404 });
})(),
);
});
完整的 Service Worker 檔案
您的 sw.js 檔案應與以下 JavaScript 類似。請注意,當更新 APP_STATIC_RESOURCES 陣列中列出的任何資源時,此 Service Worker 中唯一必須更新的常量或函式是 VERSION 的值。
// The version of the cache.
const VERSION = "v1";
// The name of the cache
const CACHE_NAME = `period-tracker-${VERSION}`;
// The static resources that the app needs to function.
const APP_STATIC_RESOURCES = [
"/",
"/index.html",
"/app.js",
"/style.css",
"/icons/wheel.svg",
];
// On install, cache the static resources
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
cache.addAll(APP_STATIC_RESOURCES);
})(),
);
});
// delete old caches on activate
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names.map((name) => {
if (name !== CACHE_NAME) {
return caches.delete(name);
}
return undefined;
}),
);
await clients.claim();
})(),
);
});
// On fetch, intercept server requests
// and respond with cached responses instead of going to network
self.addEventListener("fetch", (event) => {
// As a single page app, direct app to always go to cached home page.
if (event.request.mode === "navigate") {
event.respondWith(caches.match("/"));
return;
}
// For all other requests, go to the cache first, and then the network.
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(event.request.url);
if (cachedResponse) {
// Return the cached response if it's available.
return cachedResponse;
}
// If resource isn't in the cache, return a 404.
return new Response(null, { status: 404 });
})(),
);
});
更新 Service Worker 時,VERSION 常量不需要更新,因為 Service Worker 指令碼內容本身的任何更改都會觸發瀏覽器安裝新的 Service Worker。但是,更新版本號是一個好習慣,因為它使開發人員(包括您自己)更容易檢視當前瀏覽器中執行的 Service Worker 版本,透過 在“應用”工具中檢查快取名稱(或“源”工具)。
注意:當對任何應用程式資源(包括 CSS、HTML 和 JS 程式碼以及影像資源)進行更改時,更新 VERSION 至關重要。版本號,或 Service Worker 檔案的任何更改,是強制為使用者更新應用程式的唯一方法。
註冊 Service Worker
現在我們的 Service Worker 指令碼已完成,我們需要註冊 Service Worker。
我們首先透過使用 特性檢測 全域性 navigator 物件上是否存在 serviceWorker 屬性來檢查瀏覽器是否支援 Service Worker API。
// Does "serviceWorker" exist
if ("serviceWorker" in navigator) {
// If yes, we register the service worker
}
如果支援該屬性,我們就可以使用 Service Worker API 的 ServiceWorkerContainer 介面的 register() 方法。
if ("serviceWorker" in navigator) {
// Register the app's service worker
// Passing the filename where that worker is defined.
navigator.serviceWorker.register("sw.js");
}
雖然上述內容足以滿足 CycleTracker 應用的需求,但 register() 方法確實返回一個 Promise,該 Promise 解析為 ServiceWorkerRegistration 物件。對於更健壯的應用程式,請檢查註冊錯誤:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then(
(registration) => {
console.log("Service worker registration successful:", registration);
},
(error) => {
console.error(`Service worker registration failed: ${error}`);
},
);
} else {
console.error("Service workers are not supported.");
}
任務
開啟 index.html 並在包含 app.js 的指令碼之後、閉合 </body> 標籤之前新增以下 <script>。
<!-- Register the app's service worker. -->
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js");
}
</script>
您可以嘗試功能齊全的 CycleTracker 經期跟蹤 Web 應用,並在 GitHub 上檢視 Web 應用原始碼。是的,它確實有效,現在,它正式成為一個 PWA!
除錯 Service Worker
由於我們設定 Service Worker 的方式,一旦它被註冊,每個請求都將從快取中獲取,而不是載入新內容。在開發時,您將頻繁編輯程式碼。您可能希望定期在瀏覽器中測試您的編輯;很可能是每次儲存後。
透過更新版本號並進行硬重置
要獲取新快取,您可以更改 版本號,然後進行硬瀏覽器重新整理。進行硬重新整理的方式取決於瀏覽器和作業系統:
- 在 Windows 上:Ctrl+F5、Shift+F5 或 Ctrl+Shift+R。
- 在 macOS 上:Shift+Command+R。
- macOS 上的 Safari:Option+Command+E 清空快取,然後 Option+Command+R。
- 在移動裝置上:轉到瀏覽器(Android)或作業系統(三星、iOS)設定,在高階設定下找到瀏覽器(iOS)或網站資料(Android、三星)網站設定,刪除 CycleTracker 的資料,然後重新載入頁面。
使用開發者工具
您可能不希望每次儲存都更新版本號。在您準備將新版本的 PWA 釋出到生產環境並向所有人提供新版本 PWA 之前,您可以登出 Service Worker,而不是在儲存時更改版本號。
您可以透過單擊瀏覽器開發人員工具中的unregister按鈕來登出 Service Worker。硬重新整理頁面將重新註冊 Service Worker 並建立新快取。
![]()
在某些開發人員工具中,您可以手動登出 Service Worker,或者您可以選擇 Service Worker 的“重新載入時更新”選項,該選項會使開發人員工具在每次重新載入時重置並重新啟用 Service Worker,只要開發人員工具是開啟的。還有一個選項可以繞過 Service Worker 並從網路載入資源。此面板包括本教程未涵蓋的功能,但隨著您建立更高階的 PWA(包括同步和推送,這兩個都包含在離線和後臺操作指南中),這些功能將很有幫助。
![]()
DevTools 應用程式面板中的 Service Worker 視窗提供了一個連結,可以訪問一個彈出視窗,其中包含瀏覽器中所有已註冊 Service Worker 的列表;而不僅僅是當前選項卡中開啟的應用程式的 Service Worker。每個 Service Worker 工作人員列表都有按鈕來停止、啟動或登出該單個 Service Worker。
![]()
換句話說,當您正在開發 PWA 時,您不必為每個應用檢視更新版本號。但請記住,當您完成所有更改後,在分發更新版本的 PWA 之前,請更新 Service Worker 的 VERSION 值。如果您忘記了,所有已經安裝您的應用或甚至在未安裝的情況下訪問您的線上 PWA 的使用者,都將無法看到您的更改!
我們完成了!
PWA 的核心是一個可以安裝的 Web 應用,並且透過漸進增強使其能夠離線工作。我們建立了一個功能完備的 Web 應用。然後,我們添加了將它轉換為 PWA 所需的兩個功能——清單檔案和 Service Worker。如果您想與他人分享您的應用,請透過安全連線使其可用。或者,如果您只想自己使用週期追蹤器,請建立本地開發環境,安裝 PWA,然後享受吧!一旦安裝,您就不再需要執行 localhost。
恭喜!