Service Worker' 的生活

因為服務工作站的生命週期不明,很難掌握他們正在執行哪些工作。他們的內部工作看起來會很不透明,甚至是任意情況。請謹記,如同任何其他瀏覽器 API,服務工作處理程序行為已明確定義、指定,並盡可能提供離線應用程式,同時在不影響使用者體驗的情況下進行更新。

在開始使用 Workbox 之前,請務必瞭解 Service Worker 的生命週期,以便瞭解 Workbox 的運作原理。

定義字詞

在瞭解 Service Worker 的生命週期前,不妨先針對這個生命週期的運作方式定義一些術語。

控管範圍和範圍

想瞭解服務工作站的運作方式,關鍵就在於「掌控」。Service Worker 具有「受控制」的網頁,是可讓服務工作站代表其攔截網路要求的網頁。Service Worker 有「且」能在指定範圍內處理網頁。

範圍

視伺服器在網路伺服器上的位置而定,Service Worker 的「範圍」會有所不同。如果 Service Worker 的頁面位於 /subdir/index.html 的頁面,且位於 /subdir/sw.js,則服務工作站的範圍為 /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 已註冊完成且現在為啟用狀態,因此會控制頁面。系統會顯示表單,其中包含服務工作站的範圍、目前狀態及其網址。注意:必須重新載入頁面與範圍無關,而是服務工作站生命週期,這部分稍後將說明。
  3. 接著前往 https://service-worker-scope-viewer.glitch.me/index.html。 雖然已在此來源上註冊 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 的例項。

新 Service Worker 的生命週期

為了讓 Service Worker 可控管網頁,必須先進入現有狀態,才能進行朗讀。首先,對於沒有使用中的 Service Worker 的網站,為網站部署了新的 Service Worker,會發生什麼事。

註冊

註冊是 Service Worker 生命週期的初始步驟:

<!-- 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. 由於使用者第一次造訪網站時,不會發生已註冊的 Service Worker,因此請等到頁面完全載入後再註冊。如果 Service Worker 預先快取任何內容,就能避免頻寬爭用。
  2. 雖然 Service Worker 受到完善支援,但快速檢查功能有助於避免瀏覽器發生錯誤。
  3. 頁面完全載入後,如果系統支援 Service Worker,請註冊 /sw.js

以下為幾個重點說明:

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

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

安裝

Service Worker 會在註冊後觸發其 install 事件。install 每個 Service Worker 只會呼叫一次,而且在更新前不會再次觸發。 您可使用 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 接受承諾,並等待承諾已解決。在此範例中,此 promise 會執行兩種非同步作業:

  1. 建立新的 Cache 執行個體,並命名為 'MyFancyCache_v1'
  2. 快取建立完成後,系統會使用非同步的 addAll 方法預先快取資產網址的陣列。

如果傳遞至 event.waitUntil 的承諾遭到拒絕,安裝就會失敗。如果發生這種情況,系統會捨棄 Service Worker。

如果承諾resolve,則安裝成功,服務工作站的狀態會變更為 'installed' 並啟動。

實行

如果註冊和安裝成功,Service Worker 就會啟動,狀態會變成 'activating'。您可以在服務工作站的 activate 事件中啟用作業期間,完成工作。此事件中的常見工作是裁舊快取,但對於全新的 Service Worker,這並不會目前的關聯性,且會在服務工作站更新時補上。

如果是新的 Service 工作站,activate 會在 install 成功後立即觸發。啟用完成後,服務工作站的狀態會變為 'activated'。請注意,根據預設,新的 Service Worker 會等到下次導覽或頁面重新整理後才開始控管頁面。

處理 Service Worker 更新

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

更新時

在下列情況中,瀏覽器會檢查 Service Worker 的更新:

更新方式

知道瀏覽器更新 Service Worker 的「時間」很重要,但「方法」則是很重要的。假設服務工作站的網址或範圍未變更,目前安裝的 Service 工作站只會在內容有所變更時,才會更新至新版本。

瀏覽器會透過多種方式偵測變更:

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

瀏覽器會在這裡進行大量繁雜的工作。為確保瀏覽器能可靠地偵測 Service Worker 內容的變更,請不要要求 HTTP 快取保留,也不要變更檔案名稱。當使用者在服務工作處理程序的範圍內瀏覽新網頁時,瀏覽器會自動執行更新檢查。

手動觸發更新檢查

更新疑慮,註冊邏輯通常不應變更。但是,如果網站上的工作階段長期存在,就可能有一個例外。 這種情況會在瀏覽要求的單一頁面應用程式中發生,因為應用程式在生命週期開始時通常會遇到一個導覽要求。在這類情況下,可透過主執行緒觸發手動更新:

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

如果是傳統網站,或使用者工作階段非長期存在的情況,可能就不需要觸發手動更新。

安裝

使用 Bundler 產生靜態資產時,這些資產的名稱會包含雜湊,例如 framework.3defa9d2.js。假設其中部分資產已預先快取,以供日後離線存取。這麼做必須更新 Service Worker,才能預先快取更新後的資產:

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. 已建立金鑰為 'MyFancyCacheName_v2' 的新 Cache 執行個體。
  2. 預先快取的資產名稱已變更。

請注意,系統會一併安裝更新後的 Service Worker。這表示舊的 Service 工作站仍能控制任何開啟的頁面,且在安裝後,新的 Service 工作站會進入等待狀態,直到它啟用為止。

根據預設,如果舊的用戶端皆未受到舊的用戶端控管,系統就會啟用新的 Service Worker。如果相關網站的所有開啟分頁都關閉,就會發生這個問題。

實行

當安裝更新的 Service Worker 且等候階段結束時,系統會啟用這個工作站,並捨棄舊的 Service Worker。在已更新 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);
      }
    }));
  }));
});

舊的快取不會自行分割。 我們必須自行處理,否則風險可能會超過儲存空間配額。 由於第一個 Service Worker 的 'MyFancyCacheName_v1' 過舊,因此將快取許可清單更新為指定 'MyFancyCacheName_v2',這會一併刪除名稱不同的快取。

移除舊的快取後,activate 事件就會結束。此時,新 Service Worker 將能控制頁面,最後再取代舊頁面!

生命週期到目前為止

無論您使用 Workbox 處理 Service Worker 的部署和更新作業,還是直接使用 Service Worker API,都要支付瞭解服務工作站的生命週期。因此,服務工作處理程序的行為應該要比神秘的邏輯更合理。

如果有興趣進一步瞭解這個主題,請查看由 Jake Archibald 撰寫的文章。整個服務生命週期在各種舞步上存在著許多細微差異,但可以瞭解,使用 Workbox 時,知識會更深遠。