服務工作處理程序的思維

如何思考服務工作處理程序

Service Worker 的力量強大,絕對值得學習。這類廣告可讓您為使用者提供更優質的使用體驗。您的網站可以立即載入。離線可以運作。這個軟體可以安裝為專為特定平台設計的應用程式,不僅呈現出精緻的細節,還能隨網路拓展規模,隨心所欲。

但服務工作處理程序和我們網路開發人員習慣的不一樣。不僅容易學習,而且還多了幾隻苦事要小心。

Google Developers 與我最近合作推出 Service Workies 專案,這是讓服務人員瞭解服務工作的免費遊戲。在建構和處理所有服務工作人員的複雜工作時,我遇到了幾難。我才最瞭解到的部分,就是想出幾張描述性的隱喻。我們將在這篇文章中,探討這些心理模型,並分析我們對於癱瘓服務特性的大腦,讓服務人員既是又有挑戰性。

相同,但不同

編寫 Service Worker 的程式碼時,有許多工作感覺會很熟悉。您可以使用您最喜愛的新 JavaScript 語言功能。您可以監聽生命週期事件,就像監聽 UI 事件一樣。您可以按照慣用的承諾管理控制流程。

不過,其他 Service Worker 行為會讓你頭昏腦脹,尤其是當您重新整理頁面,但未套用程式碼變更時,更是如此。

新圖層

一般而言,建立網站時只需要考慮兩個層:用戶端和伺服器。Service Worker 是位於中間的全新層。

Service Worker 是用戶端和伺服器之間的中間層

服務工作處理程序就像是瀏覽器擴充功能,一種可以在使用者的瀏覽器中安裝。安裝完成後,Service Worker 會使用強大的中層「擴充」網站的瀏覽器。這個 Service Worker 層可以攔截和處理網站發出的所有要求。

Service Worker 層有自己的生命週期,與瀏覽器分頁無關。只重新整理網頁並不足以更新 Service Worker,只是您並不希望重新整理頁面來更新部署在伺服器上的程式碼。每個圖層都有其專屬的更新規則。

Service Workies 遊戲中,我們涵蓋了 Service Worker 生命週期的許多詳細資料,我們會針對這個生命週期提供一些實務做法。

功能強大,但功能有限

在您的網站上聘請服務人員會帶來出色的好處,網站可以:

  • 就算使用者離線,也能順暢運作
  • 透過快取大幅提升效能
  • 請使用推播通知
  • 安裝為 PWA

由於服務工作人員可以做到的程度多,他們卻受到設計限制。但無法在和您網站的同步或相同的執行緒中執行任何動作。這表示無法存取以下項目:

  • localStorage
  • DOM
  • 窗戶

好消息是,您的網頁可透過多種方式與服務工作人員溝通,包括直接 postMessage、一對一訊息管道,以及一對多廣播頻道

壽命長,但中時短暫

運作中的 Service Worker 會在使用者離開網站或關閉分頁後運作。瀏覽器會保留這個服務工作處理程序,以便使用者下次回訪您的網站時準備就緒。在第一次發出要求之前,服務工作人員有機會攔截要求並掌控網頁。這就是網站可以離線運作的原因,就算使用者沒有連上網際網路,服務工作人員也可以提供網頁本身的快取版本。

服務工作中,我們會透過 Kolohe (友善的服務工作人員) 攔截和處理要求,以視覺化方式呈現這個概念。

已停止

雖然服務工作處理程序似乎不重要,但卻幾乎隨時都可能停止。瀏覽器不要在目前未執行任何作業的 Service Worker 上浪費資源。停止作業與啟動不同,Service Worker 會保持安裝並啟用。只是想睡覺。下次需要時 (例如處理要求) 時,瀏覽器會將其喚醒。

waitUntil

因為這樣總是有可能進入休眠狀態,因此您的服務工作人員需要能夠讓瀏覽器知道哪些事情需要處理,而且不覺得小睡片刻。此時event.waitUntil()就能派上用場。這個方法會延長自身使用的生命週期,在準備就緒前,讓物件不會處於停止狀態,也不會進入生命週期的下一階段。這樣我們就有時間設定快取、從網路擷取資源。

以下範例會告知瀏覽器,在建立 assets 快取並填入 sword 圖片之前,系統並未安裝服務工作處理程序:

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

留意全球狀態

這個開始/停止作業發生時,系統會重設 Service Worker 的全域範圍。因此,請小心不要在 Service Worker 中使用任何全域狀態,否則會在下次重新啟動且狀態與預期不同時感到擔心。

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

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

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

這個 Service Worker 會在每次要求時記錄一個數字—假設 0.13981866382421893hasHandledARequest 變數也會變更為 true。現在服務工作處理程序暫時處於閒置狀態,因此瀏覽器會將其停止。下次收到要求時,服務工作處理程序必須再次,瀏覽器才會喚醒服務。系統會重新評估指令碼指令碼。現在 hasHandledARequest 已重設為 false,而 favoriteNumber 是完全不同的:0.5907281835659033

您無法依賴 Service Worker 中的儲存狀態。此外,建立訊息管道這類項目可能會造成錯誤:每次服務工作處理程序停止/開始時,您都會獲得全新的執行個體。

第 3 章中,我們將以視覺化的方式呈現已停止的服務工作人員,因為在等待喚醒期間,服務工作人員會失去所有顏色。

停止的 Service Worker 視覺化

相輔相成,

系統一次只能由一個 Service Worker「控管」您的網頁。但它可以一次「安裝」兩個 Service Worker。變更 Service Worker 程式碼並重新整理頁面後,您實際上並未編輯 Service Worker。Service Worker「無法變更」。您就是要製作全新網站。這個新的 Service Worker (讓我們命名為 SW2) 會安裝,但目前還無法啟用。必須「等待」目前的 Service Worker (SW1) 終止 (使用者離開您的網站時)。

與其他 Service Worker 的快取通訊

安裝時,SW2 可以進行設定,通常是建立及填入快取。但請注意:這個新的 Service Worker 可以存取目前 Service Worker 目前可存取的所有項目。不小心打理時,新的等候服務工作人員也可能致上目前的服務工作人員差不多。以下舉出一些可能造成問題的例子:

  • SW2 可能會刪除 SW1 正在使用的快取。
  • SW2 可以編輯 SW1 使用的快取內容,導致 SW1 回應該網頁預期之外的資產。

略過等待

服務工作處理程序也可以在安裝完成後,使用具有風險的 skipWaiting() 方法控管網頁。除非您有意嘗試更換故障的 Service Worker,否則這通常不是問題。新的 Service 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 狀態後,就知道工作已接管,而先前的 Service Worker 就會是多餘的。因此,請務必在舊的 Service Worker 之後清理。不僅會尊重使用者快取儲存空間限制,但也有可能能防止意外錯誤。

caches.match() 方法是常用的捷徑,用於從任何相符快取中擷取項目。但會依照快取的建立順序反覆檢查快取。因此,假設您在兩個不同的快取中有兩個版本的指令碼檔案 app.jsassets-1assets-2。您的網頁需使用儲存在「assets-2」中的較新指令碼。不過,如果你沒有刪除舊的快取,caches.match('app.js') 就會從 assets-1 傳回舊快取,這很可能導致你的網站損毀。

前一個 Service Worker 後所需的清除工作,就是刪除新 Service Worker 不需要的所有快取:

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 Workies,您將在當中瞭解 Service Worker 的方法,以便戰勝離線的獸。