Chrome Dev Summit 2018 is happening now and streaming live on YouTube. Watch now.

服務工作線程生命週期

服務工作線程的生命週期是非常複雜的一部分。如果您不瞭解它要做什麼以及它有哪些優勢,那麼您會感覺它讓您敗下陣來。然而,一旦您明白它的工作原理,您就可以向用戶提供幾乎無法察覺的無縫更新,從而使網絡和原生模式的優勢爲您所用。

這是一個深度教程,但每個章節開頭的項目列表包含了您需要了解的大部分內容。

目的

服務工作線程生命週期的Objective:

  • 實現離線優先。
  • 允許新服務工作線程自行做好運行準備,無需中斷當前的服務工作線程。

  • 確保整個過程中作用域頁面由同一個服務工作線程(或者沒有服務工作線程)控制。

  • 確保每次只運行網站的一個版本。

最後一點非常重要。如果沒有服務工作線程,用戶可以將一個標籤加載到您的網站,稍後打開另一個標籤。 這會導致同時運行網站的兩個版本。 有時候這樣做沒什麼問題,但如果您正在處理存儲,那麼,出現兩個標籤很容易會讓您的操作中斷,因爲它們的共享的存儲空間管理機制大相徑庭。這可能會導致錯誤,更糟糕的情況是導致數據丟失。

第一個服務工作線程

簡介:

  • install 事件是服務工作線程獲取的第一個事件,並且它僅發生一次。

  • 傳遞到 installEvent.waitUntil() 的一個 promise 可表明安裝的持續時間以及安裝是否成功。

  • 在成功完成安裝並處於“活動狀態”之前,服務工作線程不會收到 fetchpush 等事件。

  • 默認情況下,不會通過服務工作線程獲取頁面,除非頁面請求本身需要執行服務工作線程。 因此,您需要刷新頁面以查看服務工作線程的影響。

  • 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>

它註冊一個服務工作線程,並在 3 秒後添加一個小狗的圖像。

下面是它的服務工作線程,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 時提供該圖像。 不過,如果您運行上述示例,首次加載頁面時您看到的是一條小狗。 按 refresh,您將看到小貓。

作用域和控制

服務工作線程註冊的默認作用域是與腳本網址相對的 ./。 這意味着如果您在 //example.com/foo/bar.js 註冊一個服務工作線程,則它的默認作用域爲 //example.com/foo/

我們調用頁面、工作線程和共享的工作線程 clients。您的服務工作線程只能控制位於作用域內的客戶端。 在客戶端“受控制”後,它在獲取數據時將執行作用域內的服務工作線程。 您可以通過 navigator.serviceWorker.controller(其將爲 null 或一個服務工作線程實例)檢測客戶端是否受控制。

下載、解析和執行

在調用 .register() 時,您的第一個服務工作線程將進行下載。如果您的腳本在初始執行中未能進行下載、解析,或引發錯誤,則註冊器 promise 將拒絕,並捨棄此服務工作線程。

Chrome 的 DevTools 在控制檯和應用標籤的服務工作線程部分中顯示此錯誤:

服務工作線程 DevTools 標籤中顯示的錯誤

Install

服務工作線程獲取的第一個事件爲 install。該事件在工作線程執行時立即觸發,並且它只能被每個服務工作線程調用一次。 如果您更改您的服務工作線程腳本,則瀏覽器將其視爲一個不同的服務工作線程,並且它將獲得自己的 install 事件。我將在後面對更新進行詳細介紹

在能夠控制客戶端之前,install 事件讓您有機會緩存您需要的所有內容。 您傳遞到 event.waitUntil() 的 promise 讓瀏覽器瞭解安裝在何時完成,以及安裝是否成功。

如果您的 promise 拒絕,則表明安裝失敗,瀏覽器將丟棄服務工作線程。 它將無法控制客戶端。這意味着我們可以依靠 fetch 事件的緩存中存在的“cat.svg”。 它是一個依賴項。

Activate

在您的服務工作線程準備控制客戶端並處理 pushsync 等功能事件時,您將獲得一個 activate 事件。 但這不意味着調用 .register() 的頁面將受控制。

首次加載此演示時,即使在服務工作線程激活很長時間後請求 dog.svg,它也不會處理此請求,您仍會看到小狗的圖像。默認值爲 consistency,如果在頁面加載時不使用服務工作線程,那麼也不會使用它的子資源。 如果您第二次加載此演示(換言之,刷新頁面),該頁面將受控制。頁面和圖像都將執行 fetch 事件,您將看到一隻貓。

clients.claim

激活服務工作線程後,您可以通過在其中調用 clients.claim() 控制未受控制的客戶端。

下面是上面的演示的變化,其在 activate 事件中調用 clients.claim()。 首先您應該看到一隻貓。 我說“應該”是因爲這受時間約束。如果在圖像嘗試加載之前服務工作線程激活且 clients.claim() 生效,那麼,您將只看到一隻貓。

如果您使用服務工作線程加載頁面的方式與通過網絡加載頁面的方式不同,clients.claim() 會有些棘手,因爲您的服務工作線程最終會控制一些未使用它加載的客戶端。

更新服務工作線程

簡介:

  • 會觸發更新的情況:

    • 導航到一個作用域內的頁面。
    • 更新 pushsync 等功能事件,除非在前 24 小時內進行了更新檢查。

    • 調用 .register()僅在服務工作線程網址已發生變化時。

    • 在獲取更新時遵循(長達 24 小時)服務工作線程腳本上的緩存標頭。 我們將創建此選擇加入行爲,因爲它可以發現問題。 在您的服務工作線程腳本上,您可能需要 max-age 爲 0。
  • 如果服務工作線程的字節與瀏覽器已有的字節不同,則考慮更新服務工作線程。 (我們正在擴展此內容,以便將導入的腳本/模塊也包含在內。)

  • 更新的服務工作線程與現有服務工作線程一起啓動,並獲取自己的 install 事件。

  • 如果新工作線程出現不正常狀態代碼(例如,404)、解析失敗,在執行中引發錯誤或在安裝期間被拒,則系統將捨棄新工作線程,但當前工作線程仍處於活動狀態。

  • 安裝成功後,更新的工作線程將 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'));
  }
});

查看上面的演示。 您應還會看到一個貓的圖像。原因是…

Install

請注意,我已將緩存名稱從 static-v1 更改爲 static-v2。這意味着我可以設置新的緩存,而無需覆蓋舊服務工作線程仍在使用的當前緩存中的內容。

就像本機應用會爲其可執行文件綁定資源那樣,此模式會創建特定於版本的緩存。 您可能還有不屬於版本特定的緩存,如 avatars

Waiting

成功安裝服務工作線程後,更新的服務工作線程將延遲激活,直到現有服務工作線程不再控制任何客戶端。 此狀態稱爲“waiting”,這是瀏覽器確保每次只運行一個服務工作線程版本的方式。

如果您運行更新的演示,您應仍會看到一個貓的圖片,因爲 V2 工作線程尚未激活。在 DevTools 的“Application”標籤中,您會看到等待的新服務工作線程:

DevTools 顯示等待的新服務工作線程

即使在演示中您僅打開一個標籤,刷新頁面時也不會顯示新版本。 這是瀏覽器導航的工作原理導致的。當您導航時,在收到響應標頭前,當前頁面不會消失,即使此響應具有一個 Content-Disposition 標頭,當前頁面也不會消失。由於存在這種重疊情況,在刷新時當前服務工作線程始終會控制一個客戶端。

要獲取更新,需要關閉或退出使用當前服務工作線程的所有標籤。 然後,當您再次瀏覽演示時,您看到的應該是一匹馬。

此模式與 Chrome 更新的方式相似。Chrome 的更新在後臺下載,但只有在 Chrome 重啓後才能生效。 在此期間,您可以繼續使用當前版本而不會受干擾。 不過,這在開發期間卻是個難題,但 DevTools 爲我們提供了可簡化它的方法,本文後面會進行介紹

Activate

舊服務工作線程退出時將觸發 Activate,新服務工作線程將能夠控制客戶端。 此時,您可以執行在仍使用舊工作線程時無法執行的操作,如遷移數據庫和清除緩存。

在上面的演示中,我維護了一個期望保存的緩存列表,並且在 activate 事件中,我刪除了所有其他緩存,從而也移除了舊的 static-v1 緩存。

如果您將一個 promise 傳遞到 event.waitUntil(),它將緩衝功能事件(fetchpushsync 等),直到 promise 進行解析。 因此,當您的 fetch 事件觸發時,激活已全部完成。

跳過等待階段

等待階段表示您每次只能運行一個網站版本,但如果您不需要該功能,您可以通過調用 self.skipWaiting() 儘快將新工作線程激活。

這會導致您的服務工作線程將當前活動的工作線程逐出,並在進入等待階段時儘快激活自己(或立即激活,前提是已經處於等待階段)。這不能讓您的工作線程跳過安裝,只是跳過等待階段。

skipWaiting() 在等待期間調用還是在之前調用並沒有什麼不同。 一般情況下是在 install 事件中調用它:

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

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

但是,您可能想在對服務工作線程發出 postMessage() 時調用它。 例如,在用戶交互後您想要 skipWaiting()

下面是一個使用 skipWaiting() 的演示。 無需離開您就應能看到一頭牛的圖片。與 clients.claim() 一樣,它是一個競態,因此,如果新服務工作線程在頁面嘗試加載圖像前獲取數據、安裝並進行激活,那麼,您將只會看到牛。

手動更新

如前所述,在執行導航和功能事件後,瀏覽器將自動檢查更新,但是您也可以手動觸發更新。

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

如果您期望用戶可以長時間使用您的網站而不必重新加載,您需要按一定間隔(如每小時)調用 update()

避免更改服務工作線程腳本的網址

如果您讀過我的一篇有關緩存最佳做法的博文,您可能會考慮爲每個服務工作線程提供一個唯一網址。請一定不要這麼做! 對於服務工作線程,這通常是一個糟糕的做法,只會在其當前位置更新腳本。

它將給您帶來如下問題:

  1. index.htmlsw-v1.js 註冊爲一個服務工作線程。
  2. sw-v1.js 緩存並提供 index.html,因此它可以實現離線優先。
  3. 您更新 index.html,以便註冊全新的 sw-v2.js

如果您執行上述操作,用戶將永遠無法獲取 sw-v2.js,因爲 sw-v1.js 將從其緩存中提供舊版本的 index.html。 因此,您將自己置於這樣的境地:您需要更新服務工作線程才能更新服務工作線程。這真得很讓人討厭。

不過,對於上面的演示,我更改服務工作線程的網址。 這樣做是爲了進行演示,讓您可以在版本間進行切換。 在生產環境中我不會這麼做。

讓開發更簡單

服務工作線程生命週期是專爲用戶構建的,這就給開發工作帶來一定的困難。 幸運的是,我們可通過以下幾個工具解決這個問題:

Update on reload

這是我最喜歡的工具。

DevTools 顯示“update on reload”

這可使生命週期變得對開發者友好。每次瀏覽時都將:

  1. 重新獲取服務工作線程。
  2. 將其作爲新版本安裝,即使它的字節完全相同,這表示運行 install 事件並更新緩存。
  3. 跳過等待階段,因此新服務工作線程將激活。
  4. 瀏覽頁面。

這意味着每次瀏覽時(包括刷新)都將進行更新,無需重新加載兩次或關閉標籤。

Skip waiting

DevTools 顯示“'skip waiting”

如果您有一個工作線程在等待,您可以按 DevTools 中的“skip waiting”以立即將其提升到“active”。

Shift-reload

如果您強制重新加載頁面 (shift-reload),則將完全繞過服務工作線程。 頁面將變得不受控制。此功能已列入規範,因此,它在其他支持服務工作線程的瀏覽器中也適用。

處理更新

服務工作線程是作爲可擴展網頁的一部分進行設計的。 我們的想法是,作爲瀏覽器開發者,必須承認網頁開發者比我們更瞭解網頁開發。因此,我們不應提供狹隘的高級 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 as skipped waiting and become
  // the new active worker.
});

您成功了!

真是太棒了!這裏介紹了許多技術理論。未來數週,我們將深入介紹上面的一些實用的應用,敬請關注!