在 Google 建構 PWA - 第 1 部分

公布欄團隊在開發 PWA 時,瞭解到的服務工作處理程序。

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

這是一系列的網誌文章,這是 Google 公布欄團隊建構對外 PWA 時,學到的經驗。我們會在這些文章中,分享我們面臨的一些挑戰、我們如何克服這些挑戰,以及避免發生錯誤的一般建議。這並不是 PWA 的完整總覽。目的是分享我們團隊親身體驗的經驗。

在第一篇貼文中,我們會先提供一些背景資訊,然後再深入研究我們所學到的服務工作處理程序。

背景

公布欄 2017 年中至 2019 年中旬。

我們為何選擇建構 PWA

在深入說明開發程序前,讓我們先看看為什麼建構 PWA 是這項專案具有吸引力的選項:

  • 快速疊代的能力:公布公告在多個市場的前測階段,尤其實用。
  • 單一程式碼集:我們的使用者近乎平均分配 Android 和 iOS 版本PWA 可讓我們建構同時在兩個平台上運作的單一網頁應用程式。這增加了團隊的速度和影響力
  • 快速更新,不受使用者行為影響。PWA 可自動更新,以減少流向過時用戶端的數量。我們可以在極短時間內為用戶端推送出破壞性的後端變更,
  • 輕鬆整合第一方和第三方應用程式。這類整合是應用程式的必要整合,而如果是 PWA,通常只需開啟網址即可。
  • 讓使用者放心安裝應用程式。

我們的架構

我們在公告中使用了 Polymer,但所有新式架構都能正常運作。

Service Worker 瞭解情況

您無法在沒有服務工作人員的情況下執行 PWA。服務工作處理程序則為您提供許多電力,例如進階快取策略、離線功能、背景同步處理等。雖然服務工作處理程序增加了一些複雜性,但我們發現這類工作帶來的好處遠遠高於這些增加的複雜程度。

如果可以,請產生金鑰

避免手動編寫 Service Worker 指令碼。如要手動編寫服務工作站,您必須手動管理快取資源並重寫大多數服務工作站程式庫 (例如 Workbox) 使用的邏輯。

儘管如此,基於內部技術堆疊,我們無法使用程式庫產生及管理 Service Worker。我們有時會根據以下經驗教訓。詳情請參閱非產生的 Service Worker 的 Pitfalls 一節

並非所有程式庫都與 Service Worker 相容

某些 JS 程式庫假設在 Service Worker 執行時,有不如預期的運作情況。舉例來說,假設 windowdocument 可用,或使用了服務工作站無法使用的 API (XMLHttpRequest、本機儲存空間等)。請確認您的應用程式需要的所有重要程式庫都與服務工作站相容。針對這個特定的 PWA,我們想使用 gapi.js 進行驗證,但因為不支援服務工作站,所以無法使用。程式庫作者也應盡可能減少或移除不必要的 JavaScript 情境假設,以支援服務工作站的用途,例如避免使用不相容的 API 並避免全域狀態

避免在初始化期間存取 IndexedDB

初始化 Service Worker 指令碼時,請勿讀取 IndexedDB,否則可能會發生以下非預期的情況:

  1. 使用者的網頁應用程式採用 IndexedDB (IDB) 版本 N
  2. 已推送新的網頁應用程式,IDB 版本 N+1
  3. 使用者造訪 PWA,觸發新服務工作處理程序的下載作業
  4. 新的 Service Worker 會在註冊 install 事件處理常式之前從 IDB 讀取內容,觸發 IDB 升級週期從 N 到 N+1
  5. 由於使用者擁有版本 N 的舊用戶端,因此服務工作站升級程序會停滯,因為持續連線仍開啟舊版資料庫
  6. Service Worker 停止運作且從未安裝

在我們的案例中,系統在安裝 Service Worker 時快取已失效,因此如果 Service Worker 從未安裝,使用者就永遠不會收到更新的應用程式。

提高安全性

雖然 Service Worker 指令碼會在背景執行,但也有可能隨時終止,即使在 I/O 作業期間 (網路、IDB 等) 中也一樣。任何長時間執行的程序應可在任何時間點恢復。

在同步處理程序將大型檔案上傳至伺服器並儲存到 IDB 的情況下,我們解決部分上傳作業中斷的解決方案是利用內部上傳程式庫的可續傳系統,在上傳前將可續傳的上傳網址儲存至 IDB,並在上傳之前並未完成第一次完成時,使用該網址繼續上傳作業。此外,在任何長時間執行的 I/O 作業之前,狀態會儲存至 IDB,以指出每筆記錄在程序中的位置。

不要依附全域狀態

由於服務工作處理程序位於不同的結構定義,所以您可能想要存在的許多符號根本不存在。我們的許多程式碼都是在 window 內容和 Service Worker 環境中執行,例如記錄、旗標和同步處理等。程式碼必須能防禦自己使用的服務,例如本機儲存空間或 Cookie。您可以使用 globalThis,以在所有情境中運作的方式參照全域物件。此外,請謹慎使用儲存在全域變數中的資料,因為無法保證指令碼何時會終止,以及狀態會撤銷。

本機開發

Service Worker 的主要元件是在本機快取資源。但在開發期間,這與您的需求「相反」,尤其是在延遲更新時。您仍希望安裝伺服器工作站,以便對其問題進行偵錯,或與背景同步處理或通知等其他 API 搭配運作。在 Chrome 中,您可以透過 Chrome 開發人員工具完成這項設定,方法是同時勾選「Bypass for network」核取方塊 (「Application」面板 >「Service worker」窗格),同時啟用「Network」面板中的「Disable cache」核取方塊,以便一併停用記憶體快取。為了涵蓋更多瀏覽器,我們選擇採用其他解決方案,包括停用 Service Worker 中的快取功能 (在開發人員建構項目中預設為啟用)。這可確保開發人員隨時取得最新的變更,而且不會發生任何快取問題。請務必加入 Cache-Control: no-cache 標頭,以防止瀏覽器快取任何資產

燈塔

Lighthouse 提供許多適用於 PWA 的偵錯工具。它會掃描網站並產生報告,內容涵蓋 PWA、效能、無障礙功能、搜尋引擎最佳化 (SEO) 和其他最佳做法。 建議您在持續整合中執行 Lighthouse,以便在您違反 PWA 標準時收到快訊。實際上這種情況發生過一次,服務工作站並未安裝,而且在發布實際工作環境前我們都沒發現。否則,將 Lighthouse 納入 CI 後 就能避免這種情況發生

持續推送軟體更新

由於 Service Worker 可以自動更新,因此使用者無法限制升級。這樣可大幅減少舊有用戶端的數量。使用者開啟應用程式時,Service Worker 會服務舊用戶端,並延遲下載新用戶端。新用戶端下載完成後,系統會提示使用者重新整理頁面來存取新功能。即使使用者忽略了這項要求,使用者下次重新整理頁面時仍會收到新版用戶端。因此,使用者相當難拒絕更新,就像在 iOS/Android 應用程式上一樣。

我們可以在短時間內為用戶端推送破壞後端變更的時間點,一般來說,在進行破壞性變更之前,我們會預留一個月的時間,讓使用者更新至較新的用戶端。由於應用程式會在過時的情況下提供服務,因此如果使用者長時間未開啟應用程式,則較有可能存在舊版用戶端。在 iOS 上,服務工作站會在數週後撤銷,因此不會發生此情況。以 Android 來說,如要緩解這個問題,可能是因為未在過時放送,或幾週後手動將內容過期。實務上,我們從未遇到過過時用戶端的問題究竟是特定團隊的願景為何,才能達到該團隊的特定用途,但 PWA 的靈活度遠高於 iOS/Android 應用程式。

在 Service Worker 中取得 Cookie 值

有時需要存取 Service Worker 環境中的 Cookie 值。在本範例中,我們必須存取 Cookie 值來產生權杖,以驗證第一方 API 要求。在 Service Worker 中,無法使用同步 API,例如 document.cookies。您隨時可以從 Service Worker 向使用中 (視窗) 的用戶端傳送訊息,要求 Cookie 值,不過服務工作站也可能在沒有視窗型用戶端 (例如背景同步處理期間) 的情況下,在背景執行。為解決這個問題,我們在前端伺服器上建立端點,直接將 Cookie 值傳回至用戶端。Service Worker 會向這個端點發出網路要求並讀取回應,以取得 Cookie 值。

隨著 Cookie Store API 推出,支援這項功能的瀏覽器就不再需要這個解決方法,因為這個解決方案能讓使用者以非同步方式存取瀏覽器 Cookie,而且可直接由 Service Worker 使用。

非產生的服務工作站陷阱

確保如有靜態快取檔案變更時,Service Worker 指令碼會隨之變更

常見的 PWA 模式,是讓 Service Worker 在其 install 階段安裝所有靜態應用程式檔案。這樣一來,用戶端就能在所有後續造訪中直接連上 Cache Storage API 快取。只有在瀏覽器偵測到 Service worker 指令碼有某種方式變更時,系統才會安裝服務工作站,因此必須確保 Service Worker 指令碼檔案在快取檔案變更之後發生某種變化。我們已手動在 Service Worker 指令碼中嵌入靜態資源檔案集的雜湊,因此每個版本都會產生不同的 Service Worker JavaScript 檔案。Workbox 等 Service Worker 程式庫會為您自動執行這項程序。

單元測試

將事件監聽器新增至全域物件,藉此執行 Service Worker API 函式。例如:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

這可能是進行測試的困難,因為您需要模擬事件觸發條件、事件物件,等待 respondWith() 回呼,然後等待結果,最後才對結果斷言。建立這種結構更簡單的方法,就是將所有實作委派給其他檔案,以便進行測試。

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

由於針對 Service Worker 指令碼進行單元測試會遇到困難,我們盡可能保留核心服務工作站指令碼,將大部分的實作項目分割成其他模組。由於這些檔案只是標準 JS 模組,因此使用標準測試程式庫進行單元測試會比較容易。

請密切關注第 2 和第 3 部分

本系列的第 2 和第 3 部分將探討媒體管理和 iOS 特有的問題。如要進一步瞭解在 Google 建構 PWA 的相關資訊,請造訪作者個人資料,瞭解如何與我們聯絡: