Service Worker 生命週期

Jake Archibald
Jake Archibald

Service Worker 的生命週期是最複雜的部分。如果不清楚他們的目標是什麼,又有什麼好處,就感覺就像身在打擊他們。瞭解其運作方式後,您就能為使用者提供順暢且不突兀的更新,混合網路模式和原生模式的最佳元素。

雖然本篇深入說明,但每個部分開頭的項目符號都已為您補充大部分的重要資訊。

意圖

生命週期的用意為:

  • 建議您將離線功能設為優先使用。
  • 允許新 Service Worker 自行準備就緒,而不會中斷目前的服務。
  • 確保「範圍內」頁面是由相同的 Service Worker (或無 Service Worker) 控管。
  • 確認網站一次只能執行一個版本。

最後一件非常重要如果沒有 Service Worker,使用者就能先載入一個分頁到你的網站,然後再開啟另一個分頁。這可能會導致同時執行兩個版本的網站。有時沒關係,但是處理儲存空間時,很容易在兩個分頁上出現不同看法,無法改變共用儲存空間的管理方式。這可能會造成錯誤,甚至資料遺失。

第一位 Service Worker

簡單來說:

  • install 事件是 Service Worker 取得的第一個事件,而且只會發生一次。
  • 傳遞至 installEvent.waitUntil() 的承諾會指出安裝時間,以及安裝成功或失敗。
  • Service Worker 完成安裝並變為「啟用」後,才會收到 fetchpush 等事件。
  • 根據預設,網頁的擷取作業不會經過 Service Worker 的傳送,除非網頁要求本身是透過 Service Worker 進行。因此,您必須重新整理頁面,才能看到 Service Worker 的影響。
  • clients.claim() 可以覆寫這個預設值,並控制未控制的頁面。

以下 HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

它會註冊 Service Worker,並在 3 秒後新增狗的圖片。

以下是其 Service Worker sw.js

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

它會快取貓的圖片,並在收到 /dog.svg 要求時放送它。不過,如果您是執行上述範例,則在首次載入頁面時會看到一隻狗。按下「重新整理」就會看到貓。

範圍和控管

以指令碼網址來說,Service Worker 註冊的預設範圍是 ./。也就是說,如果您在 //example.com/foo/bar.js 註冊 Service Worker,其預設範圍會是 //example.com/foo/

我們稱為頁面、工作人員及共用員工。clients服務工作處理程序只能控制範圍內的用戶端。用戶端「受控制」後,擷取作業會通過範圍內 Service Worker。您可以偵測透過 navigator.serviceWorker.controller 控制的用戶端是否為空值,或是 Service Worker 執行個體。

下載、剖析及執行

呼叫 .register() 時,您的第一個 Service Worker 會下載。如果指令碼無法在初始執行時下載、剖析或擲回錯誤,註冊 promise 會拒絕,並捨棄 Service Worker。

Chrome 開發人員工具會在控制台以及應用程式分頁的「Service Worker」部分顯示錯誤:

Service Worker 開發人員工具分頁中顯示錯誤

安裝

Service Worker 取得的第一個事件為 install。worker 會在工作站執行時觸發,而且每個 Service Worker 只會呼叫一次。如果您修改服務工作站指令碼,瀏覽器會將其視為不同的 Service Worker,且會收到自己的 install 事件。我稍後會詳細說明更新內容

install 事件讓您有機會快取所有必要資料,再處理用戶端。您傳遞至 event.waitUntil() 的承諾會讓瀏覽器知道安裝完成後,以及安裝是否成功。

如果您的承諾拒絕,表示安裝失敗,瀏覽器也會擲回 Service Worker。絕不會控制用戶端。這表示我們無法仰賴 fetch 事件中快取存在的 cat.svg。是依附元件

啟用

當 Service Worker 可以控制用戶端並處理 pushsync 等功能性事件時,您會收到 activate 事件。但這不代表系統會控管名為 .register() 的網頁。

第一次載入示範時,即使在 Service Worker 啟用後很久才要求 dog.svg,系統也不會處理要求,您仍會看到狗的圖片。如果網頁在沒有 Service Worker 的情況下載入,則預設值為一致性。如果您再次載入示範 (也就是重新整理頁面),系統就會控制該示範。網頁和圖片都會經歷 fetch 事件,而你會看到一隻貓。

clients.claim

啟用服務工作處理程序後,即可呼叫 clients.claim(),控制未受控制的用戶端。

以下是上述示範的變化版本,會在其 activate 事件中呼叫 clients.claim()。你第一次看到貓咪圖片。我說「應該」,因為時間很敏感。只有在 Service Worker 啟動時,且 clients.claim() 會在嘗試載入圖片前生效,您才會顯示貓咪。

如果您使用服務工作處理程序載入網頁,而不是透過網路載入網頁,clients.claim() 可能會產生麻煩,因為您的服務工作人員會控制沒有該網路載入的某些用戶端。

更新 Service Worker

簡單來說:

  • 如果發生下列任一情況,就會觸發更新:
    • 瀏覽範圍內頁面。
    • 功能事件,例如 pushsync,但如果過去 24 小時內有更新檢查。
    • 只有在 Service Worker 網址變更時,才呼叫 .register()。不過,您應避免變更工作站網址
  • 根據預設,Chrome 68 以上版本 (包括 Chrome 68 以上版本) 在檢查已註冊的 Service Worker 指令碼更新時,會略過快取標頭。透過 importScripts() 擷取服務工作站內部載入的資源時,這些方法仍會遵循快取標頭。您可以在註冊 Service Worker 時設定 updateViaCache 選項,藉此覆寫這個預設行為。
  • 如果服務工作處理程序的位元組與瀏覽器既有的位元組不同,系統就會視為已更新服務工作處理程序。(我們也將進一步納入匯入的指令碼/模組)。
  • 更新版 Service Worker 會與現有事件一起啟動,並取得專屬的 install 事件。
  • 如果新 worker 的狀態碼 (例如 404)、無法剖析、執行期間擲回錯誤,或在安裝期間拒絕,系統會捨棄新工作站,但目前的工作站依然有效。
  • 安裝成功後,更新後的 worker 會wait,直到現有的工作站控制零個用戶端為止。(請注意,用戶端在重新整理期間會重疊)。
  • self.skipWaiting() 可防止等待作業,代表服務工作處理程序會在安裝完成後立即啟動。

假設我們修改了服務工作人員指令碼,改為使用馬圖片而非貓的圖片回應:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

查看上方示範。您仍可以看到貓咪的圖片。原因如下...

安裝

請注意,我已將快取名稱從 static-v1 變更為 static-v2。這表示,我可以設定新快取,而不會覆寫目前 Service Worker 中的資料,而舊版 Service Worker 也仍使用中的資料。

這個模式會建立特定版本專屬的快取,類似於原生應用程式會搭配其執行檔所封裝的資產。您也可能有非特定版本的快取,例如 avatars

等待中

安裝成功後,更新的 Service Worker 會延遲啟用,直到現有的 Service Worker 不再控制用戶端為止。這個狀態稱為「等待中」,瀏覽器可以確保一次只會執行一個版本的 Service Worker。

如果您執行更新版的示範,您應該會看到貓咪圖片,因為 V2 工作站尚未啟用。您可以在開發人員工具的「應用程式」分頁中看到正在等待的新服務工作站:

開發人員工具顯示正在等候的新 Service Worker

即使您只有一個分頁開啟示範模式,重新整理頁面也不足以使新版本接管。這取決於瀏覽器導覽的運作方式。進行導覽時,在收到回應標頭前,目前的頁面不會消失,即使回應含有 Content-Disposition 標頭,目前頁面可能仍會保留。由於這個問題重疊,目前的服務工作處理程序會在重新整理期間一直控制用戶端。

如要更新,請使用目前的 Service Worker,關閉或離開所有分頁。之後,當您再次前往示範頁面時,應該會看見馬路。

這個模式與 Chrome 的更新方式類似。Chrome 會在背景下載更新,但要等 Chrome 重新啟動才能套用。在這段期間,您可以繼續使用目前的版本,不必擔心服務中斷的問題。不過,這在開發過程中難免令人感到痛苦,但開發人員工具能簡化開發程序,詳情請參閱本文後續說明

啟用

這會在舊的 Service Worker 消失後觸發,而新的 Service Worker 就能控制用戶端。現在,您可以趁著舊的 worker 仍在使用中時,處理自己無法完成的工作,例如遷移資料庫及清除快取。

在上述示範中,我保有一連串預期的快取清單。在 activate 事件中,我將刪除其他任何快取,並一併移除舊的 static-v1 快取。

如果您將承諾傳遞至 event.waitUntil(),它會緩衝功能事件 (fetchpushsync 等),直到 promise 解決為止。因此,當 fetch 事件觸發時,代表已經完全啟用。

略過等候階段

等候階段表示您一次執行的網站版本只執行一個版本,但如果您不需要該功能,可以呼叫 self.skipWaiting(),讓新的服務工作處理程序更快啟用。

這會導致服務工作處理程序啟動目前的工作站,並在進入等候階段後立即啟動 (或者如果已在等候階段中立即啟動)。它不會導致工作站略過安裝程序,只是等待。

只要呼叫 skipWaiting() 是在等候期間或等待之前,就沒有關係。在 install 事件中呼叫這個方法很常見:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

不過,您可能會想要將其做為 postMessage() 對 Service Worker 的結果呼叫。就像這樣,您希望在使用者互動之後使用 skipWaiting()

以下是使用 skipWaiting() 的示範內容。你應該會看到不需要離開的牛隻相片。就像 clients.claim() 是種族一樣,所以只有在新 Service Worker 擷取、安裝及啟動圖片之後,頁面嘗試載入圖片時,您才會看到牛奶。

手動更新

如先前所述,瀏覽器會在導覽和功能事件完成後自動檢查更新,但您也可以手動觸發更新:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

如果您預期使用者會長時間使用網站,而不重新載入,建議您每隔一段時間 (例如每小時) 呼叫 update()

避免變更服務工作處理程序指令碼的網址

如果您已看過我的快取最佳做法貼文,可以考慮為服務工作人員的每個版本提供專屬網址。請不要這樣做!對服務工作人員來說,這通常是不當的做法,只需更新目前位置的指令碼即可。

這個頁面可能會有以下問題:

  1. index.htmlsw-v1.js 註冊為 Service Worker。
  2. sw-v1.js 會快取並提供 index.html,因此可以離線優先。
  3. 您必須更新 index.html,才能註冊全新的 sw-v2.js

如果執行上述操作,使用者永遠不會收到 sw-v2.js,因為 sw-v1.js 是從快取提供舊版的 index.html。您目前的位置必須要更新服務工作人員,以便更新服務工作處理程序。呃,

但是,在上方的示範中,我已經變更 Service Worker 的網址。因此,為了方便示範,您可以在版本間切換。我無法在實際工作環境中使用。

簡化開發程序

服務工作處理程序的生命週期為使用者著想,但是在開發期間會有些許問題。幸好以下有幾項工具可以提供協助:

重新載入時更新

這是我最愛的禮物。

開發人員工具顯示「在重新載入時更新」

這會將生命週期變更為方便開發人員使用。每個導覽項目都會:

  1. 重新擷取 Service Worker。
  2. 即使其位元組相同,請將其安裝為新版本。也就是說,install 事件會執行,快取也會更新。
  3. 略過等候階段,啟用新的 Service Worker。
  4. 瀏覽頁面。

這表示每次瀏覽時 (包括重新整理),都能取得更新內容,而不需要重新載入兩次或關閉分頁。

略過等候程序

開發人員工具顯示「略過等待中」

如果有工作站在等待,您可以在開發人員工具中按一下「略過等待」,立即將其升級為「啟用」。

Shift-重新載入

如果您強制重新載入頁面 (shift-重新載入),則會完全略過 Service Worker。它無法控制。這項功能在規格中,因此可在其他支援服務工作站的瀏覽器中執行。

處理更新

Service Worker 是可延伸網路的一部分而設計。因為我們身為瀏覽器開發人員,認為我們不比網頁程式開發人員更熟。因此,我們不會提供採用「我們」模式來解決特定問題的狹窄高階 API,而是讓您存取瀏覽器質感,讓您以最適合「您的」使用者的方式存取瀏覽器。

因此,為了盡可能啟用更多的模式,可以觀察整個更新週期:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

整個生命週期

如您所見,瞭解 Service Worker 的生命週期會帶來好處。理解服務工作處理程序的行為應該要更符合邏輯,也較不令人印象深刻。這些知識可讓您在部署及更新 Service Worker 時更有信心。