Service Worker' 的生活

如不瞭解服務工作站的生命週期,就很難瞭解服務工作站在做什麼。內部運作方式似乎不透明,甚至是任意。請記住,與其他瀏覽器 API 一樣,服務工作者行為已明確定義及指定,可讓離線應用程式運作,同時也能協助更新,而不會中斷使用者體驗。

在深入瞭解 Workbox 之前,請務必先瞭解服務工作程生命週期,這樣才能瞭解 Workbox 的運作方式。

定義條款

在深入瞭解服務工作者生命週期之前,建議您先定義一些關於生命週期運作方式的術語。

控制項和範圍

控制概念對於瞭解服務工作者如何運作至關重要。若網頁說明為由 Service Worker 控管,表示該網頁允許 Service Worker 代為攔截網路要求。Service Worker 會出現,並可在指定範圍內為網頁執行工作。

範圍

服務工作者的範圍取決於其在網路伺服器上的所在位置。如果 Service Worker 在位於 /subdir/index.html 的網頁上執行,且位於 /subdir/sw.js,則 Service Worker 的範圍為 /subdir/。如要瞭解範圍的實際運作方式,請參考以下範例:

  1. 前往 https://service-worker-scope-viewer.glitch.me/subdir/index.html。系統會顯示訊息,指出沒有 Service Worker 控管該網頁。不過,該頁面會註冊 https://service-worker-scope-viewer.glitch.me/subdir/sw.js 中的 Service Worker。
  2. 重新載入頁面。由於 Service Worker 已註冊並處於啟用狀態,因此會控管網頁。您會看到包含服務 worker 範圍、目前狀態和網址的表單。注意:需要重新載入頁面與範圍無關,而是與服務工作者生命週期有關,我們會在後面說明。
  3. 接著前往 https://service-worker-scope-viewer.glitch.me/index.html。即使 Service Worker 已在這個來源註冊,仍會顯示訊息,指出目前沒有 Service Worker。這是因為這個網頁不在註冊的 Service Worker 範圍內。

範圍會限制 Service Worker 控管的網頁。在本例中,這表示從 /subdir/sw.js 載入的 Service Worker 只能控制位於 /subdir/ 或其子樹狀結構中的網頁。

上述是預設的範圍設定方式,但您可以設定 Service-Worker-Allowed 回應標頭,並將 scope 選項傳遞至 register 方法,藉此覆寫許可的最大範圍。

除非有非常充分的理由,將 Service Worker 範圍限制為來源的子集,否則請從網路伺服器的根目錄載入 Service Worker,讓其範圍盡可能廣泛,而不用擔心 Service-Worker-Allowed 標頭。這樣對所有人來說都會簡單許多。

用戶端

當我們說 Service Worker 控管網頁時,其實是指控管用戶端。用戶端是指網址位於服務工作者範圍內的任何已開啟的網頁。具體來說,這些是 WindowClient 的例項。

新服務工作者的生命週期

為了讓服務工作者控制網頁,必須先將服務工作者帶入。我們先從為沒有啟用服務工作的網站部署全新服務工作時,會發生什麼事開始說明。

註冊

註冊是服務工作站生命週期中的初始步驟:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

這個程式碼會在主執行緒上執行,並執行下列操作:

  1. 由於使用者首次造訪網站時並未註冊服務工作者,因此請等待網頁完全載入後再註冊。這樣可避免服務工作站預先快取任何內容時,發生頻寬爭用情形。
  2. 雖然 Service Worker 受到廣泛支援,但快速檢查有助於避免在未支援 Service Worker 的瀏覽器中發生錯誤。
  3. 網頁完全載入後,如果支援服務工作者,請註冊 /sw.js

請務必瞭解以下幾點:

  • 服務工作者只能透過 HTTPS 或 localhost 使用
  • 如果服務工作者的內容含有語法錯誤,註冊作業就會失敗,且服務工作者會遭到捨棄。
  • 提醒:Service Worker 會在特定範圍內運作。此處的範圍是整個來源,因為來源是從根目錄載入。
  • 註冊開始時,服務工作站狀態會設為 'installing'

註冊完成後,系統就會開始安裝。

安裝

服務工作者會在註冊後觸發 install 事件。每個服務工作者只會呼叫 install 一次,且在更新前不會再次觸發。您可以使用 addEventListener,在 worker 的範圍內註冊 install 事件的回呼:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

這會建立新的 Cache 例項,並預先快取素材資源。我們稍後會進一步討論預先快取功能,因此現在就讓我們專注於 event.waitUntil 的角色。event.waitUntil 會接受承諾,並等待承諾解析。在本範例中,該應許會執行兩項非同步作業:

  1. 建立名為 'MyFancyCache_v1' 的新 Cache 例項。
  2. 建立快取後,系統會使用非同步 addAll 方法,預先快取資產網址陣列。

如果傳遞至 event.waitUntil 的承諾遭到拒絕,安裝作業就會失敗。在這種情況下,系統會捨棄服務工作者。

如果承諾解析,則安裝作業會成功,服務工作者的狀態會變更為 'installed',然後啟用。

啟用

如果註冊和安裝成功,服務工作者就會啟用,其狀態會變成 'activating'。在服務工作者的 activate 事件中啟用時,可以執行工作。這個事件的典型工作是刪除舊快取,但對於全新的服務工作架構來說,這項工作目前不相關,我們會在討論服務工作架構更新時進一步說明。

對於新的服務工作者,activate 會在 install 成功後立即觸發。啟用完成後,服務工作者的狀態會變成 'activated'。請注意,根據預設,新 Service Worker 會在下一次導覽或網頁重新整理時才開始控管網頁。

處理服務工作站更新

第一個服務工作者部署後,可能需要稍後更新。舉例來說,如果要求處理或預先快取邏輯發生變更,可能就需要更新。

更新時間

瀏覽器會在下列情況下檢查服務工作者的更新:

更新的運作方式

瞭解瀏覽器更新服務工作單元的「時機」固然重要,但「方式」也同樣重要。假設服務工作者的網址或範圍未變更,目前已安裝的服務工作者只會在內容變更時更新為新版本。

瀏覽器偵測變更的方式有幾種:

  • importScripts 要求的任何位元組對位元組變更 (如適用)。
  • 服務工作者的頂層程式碼有任何變更,都會影響瀏覽器為其產生的指紋。

瀏覽器在這裡負責許多繁重的工作。為確保瀏覽器擁有所有必要資訊,可可靠地偵測服務工作程內容的變更,請勿告知 HTTP 快取保留該內容,也不要變更其檔案名稱。當服務工作者範圍內有導向至新頁面的導覽時,瀏覽器會自動執行更新檢查。

手動觸發更新檢查

至於更新,註冊邏輯通常不應變更。不過,如果網站上的工作階段持續時間很長,就可能會出現例外狀況。這種情況可能發生在單頁應用程式中,因為應用程式通常會在應用程式生命週期一開始時遇到一個導覽要求。在這種情況下,您可以在主執行緒上觸發手動更新:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

對於傳統網站,或在使用者工作階段不長的情況下,可能不需要觸發手動更新。

安裝

使用 Bundler 產生靜態素材資源時,這些素材資源的名稱會包含雜湊,例如 framework.3defa9d2.js。假設部分素材資源已預先快取,以便日後離線存取。這需要服務工作程更新,才能預先快取更新的素材資源:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

與前述第一個 install 事件範例相比,這裡有兩個不同之處:

  1. 系統會建立一個 Cache 例項,其鍵為 'MyFancyCacheName_v2'
  2. 預先快取的資產名稱已變更。

請注意,更新後的服務工作站會與先前的服務工作站一併安裝。也就是說,舊的服務工作者仍會控制所有已開啟的網頁,而安裝後,新的服務工作者會進入等待狀態,直到啟用為止。

根據預設,如果沒有任何用戶端受舊服務工作者控制,系統就會啟用新的服務工作者。當相關網站的所有開啟分頁都關閉時,就會發生這種情況。

啟用

安裝更新後,等待階段結束時,系統就會啟用更新的服務工作者,並捨棄舊的服務工作者。在更新的 Service Worker activate 事件中執行的常見工作,是刪除舊快取。使用 caches.keys 取得所有已開啟 Cache 例項的金鑰,然後使用 caches.delete 刪除不在已定義的許可清單中的快取,即可移除舊快取:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

舊快取不會自行整理。我們必須自行執行這項操作,否則可能會超過儲存空間配額。由於第一個服務工作者的 'MyFancyCacheName_v1' 已過時,因此快取允許清單會更新為指定 'MyFancyCacheName_v2',這會刪除名稱不同的快取。

舊快取移除後,activate 事件就會結束。此時,新 Service Worker 會接管網頁,並最終取代舊 Service Worker!

生命週期會持續進行

無論是使用 Workbox 處理服務工作者部署和更新作業,還是直接使用服務工作者 API,都建議您瞭解服務工作者生命週期。瞭解這一點後,服務工作者行為就會顯得更有邏輯性,而非神秘莫測。

如果您想深入瞭解這個主題,不妨參考 Jake Archibald 撰寫的這篇文章。服務生命週期有許多細微差異,但可以瞭解,這項知識在使用 Workbox 時會非常實用。