服務工作處理程序的思維

如何思考服務工作者。

服務工作者功能強大,絕對值得學習。讓您為使用者提供全新等級的體驗。網站可以立即載入。可離線運作。這類應用程式可做為特定平台應用程式安裝,並提供精緻的使用體驗,同時享有網頁的觸及範圍和自由度。

但服務工作站與大多數網頁開發人員所熟悉的任何東西都不同。這些工具的學習曲線陡峭,而且有許多需要留意的陷阱。

我和 Google 開發人員最近合作完成了一個專案:Service Workies,這是一款免費遊戲,可讓您瞭解 Service Worker。在建構服務 worker 的複雜內部和外部作業時,我遇到了一些問題。對我來說最有幫助的是,我想到了一些描述性的比喻。在本篇文章中,我們將探討這些心智模型,並深入瞭解服務端代理程式既複雜又強大的矛盾特徵。

相同但不同

編寫服務工作程式碼時,您會發現許多內容都很熟悉。您可以使用自己最喜歡的全新 JavaScript 語言功能。監聽生命週期事件的方式與 UI 事件相同。您可以使用承諾管理控制流程,就像以往一樣。

但其他服務工作程式行為會讓您一頭霧水。尤其是在重新整理頁面後,程式碼變更未套用時。

新圖層

通常在建構網站時,您只需要考慮兩個層面:用戶端和伺服器。服務工作者是位於中間的全新層級。

服務工作者是用戶端與伺服器之間的中介層

您可以將 Service Worker 視為一種瀏覽器擴充功能,也就是網站可在使用者瀏覽器中安裝的擴充功能。安裝後,服務工作者會使用強大的中介層擴充網站的瀏覽器。這個服務工作架構層可攔截並處理網站發出的所有要求。

服務工作者層有自己的生命週期,不受瀏覽器分頁影響。簡單的網頁重新整理作業不足以更新服務工作者,就像您不會期待網頁重新整理作業會更新在伺服器上部署的程式碼一樣。每個圖層都有各自的更新規則。

在「Service Workies」遊戲中,我們會介紹服務 worker 生命週期的許多細節,並提供大量練習題,讓您練習使用服務 worker。

功能強大,但受限

在網站上使用 Service Worker 可帶來許多好處。你的網站可以:

  • 即使使用者處於離線狀態,也能正常運作
  • 透過快取大幅提升效能
  • 使用推播通知
  • PWA 形式安裝

雖然服務工作程式功能多元,但設計上仍有限制。他們無法執行任何同步作業,也無法在與您的網站相同的執行緒中執行作業。也就是說,您無法存取以下內容:

  • localStorage
  • DOM
  • 視窗

好消息是,網頁可以透過幾種方式與服務工作程式通訊,包括直接 postMessage、一對一訊息管道和一對多廣播管道

長期,但壽命短

即使使用者離開網站或關閉分頁,仍會保留服務工作程式。瀏覽器會保留這個 Service Worker,以便在使用者下次造訪網站時使用。在首次要求發出之前,服務工作架構會有機會攔截該要求,並取得網頁的控制權。這就是讓網站可在離線狀態下運作的關鍵,因為 Service Worker 可提供頁面的快取版本,即使使用者沒有網際網路連線也沒關係。

服務工作程式中,我們透過 Kolohe (友善的服務工作程式) 攔截及處理要求,以圖像化方式呈現這個概念。

已停止

雖然 Service Worker 似乎永遠不會停止運作,但幾乎隨時都會停止。瀏覽器不想在目前沒有任何動作的 Service Worker 上浪費資源。停止與終止不同,服務工作者仍會安裝並啟用。只是讓裝置進入休眠狀態。下次需要時 (例如處理要求),瀏覽器就會喚醒該元件。

waitUntil

由於服務工作者可能會持續處於休眠狀態,因此需要透過某種方式讓瀏覽器知道何時正在執行重要作業,而不需要休眠。這時 event.waitUntil() 就能派上用場。這個方法會延長所用生命週期的時間,在我們準備就緒之前,避免生命週期停止或進入下一個階段。這樣一來,我們就能有時間設定快取、從網路擷取資源等。

這個範例會告訴瀏覽器,在建立 assets 快取並填入劍的圖片之前,服務工作站的安裝作業不會完成:

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

留意全域狀態

當這項啟動/停止作業發生時,服務工作者的全域範圍就會重設。因此請務必注意,不要在服務工作者中使用任何全域狀態,否則下次服務 worker 喚醒時,如果狀態與預期不同,您就會感到失望。

請參考以下使用全域狀態的範例:

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

這個服務工作站會針對每個要求記錄一個數字,假設為 0.13981866382421893hasHandledARequest 變數也會變更為 true。服務工作站現在處於閒置狀態,因此瀏覽器會停止它。下次有要求時,系統會再次需要 Service Worker,因此瀏覽器會喚醒 Service Worker。系統會再次評估指令碼。hasHandledARequest 會重設為 false,而 favoriteNumber 則會變成完全不同的 0.5907281835659033

您無法依賴服務工作站中的儲存狀態。此外,建立 Message Channels 等項目的例項可能會導致錯誤:每次服務工作程式停止/啟動時,您都會取得全新的例項。

第 3 章「Service Worker」中,我們將停止的 Service Worker 視為在等待喚醒時失去所有顏色。

已停止的服務工作者視覺化圖表

一起,但分開

您的網頁一次只能由一個 Service Worker 控管。但可以同時安裝兩個服務工作者。當您變更服務工作者程式碼並重新整理網頁時,您實際上並未編輯服務工作者。服務工作者無法變更immutable。而是建立全新的帳戶。這個新的服務工作者 (我們稱之為 SW2) 會安裝,但不會啟用。必須等待目前的服務工作架構 (SW1) 結束 (使用者離開網站時)。

干擾其他服務工作程的快取

在安裝期間,SW2 可以進行設定,通常是建立並填入快取。不過請注意,這個新的服務工作程式可存取目前服務工作程式可存取的所有內容。如果你不小心,新的等待中 Service Worker 可能會對目前的 Service Worker 造成嚴重影響。以下是可能會導致你遇到問題的例子:

  • SW2 可能會刪除 SW1 正在使用的快取。
  • SW2 可能會編輯 SW1 使用的快取內容,導致 SW1 回應網頁未預期的素材資源。

略過 skipWaiting

Service Worker 也可以使用風險較高的 skipWaiting() 方法,在安裝完成後立即控管網頁。除非您刻意要取代有錯誤的服務 worker,否則這通常不是個好主意。新的服務工作者可能會使用目前網頁未預期的更新資源,導致錯誤和錯誤。

清除原始內容

如要避免服務工作者彼此衝突,請務必確保它們使用不同的快取。最簡單的方法就是為所使用的快取名稱建立版本。

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

部署新的 Service Worker 時,您會提升 version,讓它使用與先前 Service Worker 完全獨立的快取,執行所需的作業。

快取的視覺化呈現

結束清理

服務工作者達到 activated 狀態後,您就知道它已接管,而先前的服務工作者已不必要。此時,請務必清理舊服務工作程式。這不僅能尊重使用者的快取儲存空間限制,還能避免不經意產生的錯誤。

caches.match() 方法是常用的捷徑,可從任何快取中擷取相符的項目。但會按照建立順序逐一檢查快取。假設您在兩個不同的快取 (assets-1assets-2) 中,有兩個版本的 app.js 指令碼檔案。您的網頁預期會使用儲存在 assets-2 中的新版指令碼。但如果您尚未刪除舊快取,caches.match('app.js') 就會從 assets-1 傳回舊快取,很可能會導致網站發生錯誤。

在先前的服務工作站後進行清理時,只需刪除新服務工作站不需要的任何快取:

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

雖然要避免 Service Worker 彼此衝突需要花費一些心力和紀律,但這一切都是值得的。

Service Worker 心態

在思考服務工作站時,請先調整正確的思維,這樣才能充滿信心地建構服務工作站。一旦掌握這些概念,您就能為使用者打造絕佳的體驗。

如果您想透過玩遊戲來瞭解這一切,那麼您就來對地方了!請前往玩Service Workies,瞭解如何使用服務工作架構來消滅離線怪獸。