【Service Worker】生命週期那些事兒

文章來自 個人博客 html

生命週期是 Service Worker 中比較複雜的一部分,若是你不知道它什麼時間將要作什麼,以及它帶來的好處,那麼你可能會有一種感受:就是它一直在和你較勁。若是理解它的工做機制,你就能夠給用戶提供完美的,無感知的更新體驗。 這篇文章是 Chrome 團隊最近總結的一片文章,配合例子講述生命週期,讓咱們更容易理解,也解決了我以前開發中遇到的一些困惑,因此決定翻譯出來。此處 閱讀原文 git

目的

本文中介紹利用生命週期能夠實現的功能大概有以下幾點:github

  • 實現緩存優先(offline-first)web

  • 在不打斷現有 SW 的狀況下,準備好一個新的 SW瀏覽器

  • 讓註冊 SW 的頁面同一時間只歸屬同一個 SW 控制緩存

  • 確保你的網站只有一個版本在運行app

最後一點尤爲重要,通常狀況下(沒有 SW 的狀況),用戶瀏覽你的網站時可能先打開一個 tab,過了一下子又打開了一個 tab,結果就是在同一時間,你的頁面運行了兩個版本,大部分時候,這樣是沒問題的,可是若是你使用了緩存,那麼兩個 tab 就要面臨如何管理緩存的問題,若是處理很差,它可能會形成異常,嚴重的形成數據丟失。svg

用戶很是不喜歡數據丟失,這會讓他們很是憂桑。工具

第一個 Service Worker

  • install 事件是 SW 觸發的第一個事件,而且僅觸發一次。fetch

  • installEvent.waitUntil()接收一個 Promise 參數,用它來表示 SW 安裝的成功與否。

  • SW 在安裝成功並激活以前,不會響應 fetchpush等事件。

  • 默認狀況下,頁面的請求(fetch)不會經過 SW,除非它自己是經過 SW 獲取的,也就是說,在安裝 SW 以後,須要刷新頁面纔能有效果。

  • clients.claim()能夠改變這種默認行爲。

舉個例子:

<!DOCTYPE html>
3秒後將出現一張圖片:
<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>

這裏註冊了一個 SW,而且在 3 秒後在頁面上添加一個圖片。

這是 SW 的代碼:

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

  // 這裏緩存一個 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);

  //若是是同域而且請求的是 dog.svg 的話,那麼返回 cat.svg
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

這裏 SW 緩存了 cat.svg,並當請求 dog.svg 的時候返回它。然而,當你運行 這個例子 的時候,你會發現第一次出現一隻狗,刷新後,貓纔會出現。

做用域與控制

SW 的默認做用域爲基於當前文件 URL 的 ./。意思就是若是你在//example.com/foo/bar.js裏註冊了一個 SW,那麼它默認的做用域爲 //example.com/foo/

咱們把頁面,workers,shared workers 叫作clients。SW 只能對做用域內的clients有效。一旦一個client被「控制」了,那麼它的請求都會通過這個 SW。咱們能夠經過查看navigator.serviceWorker.controller是否爲 null 來查看一個client是否被 SW 控制。

下載-解析-執行

當你調用.register()的時候,第一個 SW 被下載下來,這過程當中若是下載,解析或者在初始化中有錯誤的話,那麼 register 的Promise 會返回 reject,而後 SW 會被銷燬。

Chrome 開發者工具會展現出來錯誤,在 Application 中的 Service Workers Tab:

alt

安裝(install)

SW 首先會觸發install,每一個 SW 只會被觸發一次,當你修改你的 SW 後,瀏覽器會認爲這是一個新的 SW,從而會再觸發這個新 SW 的install事件,在後面會詳細說到。

install是在 SW 控制 clients以前處理緩存很好的時機。在 event.waitUntil()傳入的 Promise 會讓瀏覽器知道 SW 何時安裝成功以及是否成功。

當 Promise reject 的時候,表明着安裝失敗,瀏覽器將這個 SW 廢棄掉,不會控制任何 clients。

激活(Activate)

安裝成功後並激活(activate)成功後,SW 就能夠處理「功能性的事件「了,好比push,sync但這並不表明調用.register()的頁面會當即生效。

第一次你請求 這個demo 的時候,雖然在 SW 被激活後好久才請求了dog.svg(由於這裏等待了三秒),但 SW 也並無處理這個請求,結果你看見了一隻狗。當你第二次請求的時候,也就是刷新頁面,這時請求被處理了,當前頁面和圖片都通過了 SW 的 fetch事件,因此你看見了一隻貓。

clients.claim

你能夠在activate事件中經過調用clients.claim()來讓沒被控制的 clients 受控。

好比 這個demo ,可能第一次你就會看見一隻貓,這裏我說「可能」,是由於這時時間敏感的,僅當 SW 激活而且clients.claim()被調用成功在圖片請求以前的時候才能夠。

因此,可想而知,當你用 SW 加載與正常請求不一樣資源的時候(好比上面的例子),那用clients.claim()可能會遇到一些問題,這時有些資源可能不會經過你的SW。

我見過不少人在代碼中把clients.claim()當作了必選項,但我本身不多這樣作,由於僅僅是第一次加載不會經過 SW,並且頁面仍是都會正常運行的。

更新 Service Worker

簡單來講:

  • 觸發更新的幾種狀況:

    • 第一次導航到做用域範圍內頁面的時候

    • 當在24小時內沒有進行更新檢測而且觸發功能性時間如pushsync的時候

    • SW 的 URL 發生變化並調用.register()

  • 當 SW 代碼發生變化,SW 會作更新(還將包括引入的腳本)

  • 更新後的 SW 會和原始的 SW 共同存在,並運行它的install

  • 若是新的 SW 不是成功狀態,好比 404,解析失敗,執行中報錯或者在 install 過程當中被 reject,它將會被廢棄,以前版本的 SW 仍是激活狀態不變。

  • 一旦新 SW 安裝成功,它會進入wait狀態直到原始 SW 不控制任何 clients。

  • self.skipWaiting()能夠阻止等待,讓新 SW 安裝成功後當即激活。

<div style="max-width:600px;">

<iframe id="iframe-2" onload="iframeLoaded('iframe-2')" style="width:100%;border:none;" src="https://www.zhengqingxin.com/static/demo/5-lifecycle/iframe2.html">
</iframe>

</div>

下面咱們來舉個 SW 更新的例子,這是一個將貓變成馬的故事:

const expectedCaches = ['static-v2'];

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

  // 將 horse.svg 緩存在新的緩存 static-v2 中
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // 刪除額外的緩存,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);

  //若是是同域而且請求的是 dog.svg 的話,那麼返回 horse.svg

  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

若是你打開 這個 demo ,你會看到一隻喵,緣由是這樣的...

install

注意這裏咱們將緩存從 static-v1 換到了 static-v2,這表明了我用了一個新的緩存空間覆蓋了以前 SW 正在使用的緩存。

這裏新建了一塊緩存的作法相似於原生 app 中將每塊資源打包到一塊指定的執行空間的作法,有時候結合實際狀況,你也能夠不這麼作。

Waiting

一旦新 SW 安裝成功,它會進入wait狀態直到原始 SW 不控制任何 clients。這個狀態是waiting,這也是瀏覽器確保在同一時間只有一個版本的 SW 運行的機制。

若是你再次打開 這個 demo ,你仍是會看到一隻喵,由於新的 SW 仍是沒有被激活,在開發者工具裏你依然看到它是 waiting 狀態。

alt

儘管這個例子中你僅打開了一個 tab,但刷新頁面並無用,這是因爲瀏覽器自己的機制,當你刷新的時候,當前頁面不會離開,直到收到了一個響應頭,並且即便這樣,若是響應中包含Content-Disposition的話,當前頁面仍是不會離開。因爲這個時間上的重疊,在刷新的時候當前的 SW 老是控制了一個 client。

爲了讓 SW 更新,你須要把全部用原始 SW 的頁面 tab 關閉或者跳轉走,這時你再訪問 這個 demo ,你就會看到了一匹野馬。

這種機制相似於 Chrome 自己的更新機制,Chrome 在後臺更新,只有當你重啓瀏覽器的時候纔會生效,在這期間你不會被打擾,能夠繼續使用當前版本。然而,這樣可能會使咱們開發者比較痛苦,好在開發者工具幫咱們解決了這個事情,後面會說到。

Activate

Activate 在舊的 SW 離開時會被觸發,這時新的 SW 能夠控制 clients。這時候你能夠作一些在老 SW 運行時不能作的事情,好比清理緩存。

在上面的例子中,以前保留的緩存,在activate時間執行的時候被清理掉。

這裏最好不要更新之前的版本,而是直接分配新的緩存空間。

若是你在event.waitUntil()中傳入了一個 Promise,SW 將會緩存住功能性事件(fetch,push,sync等等),直到 Promise 返回 resolve 的時候再觸發,也就是說,當你的fetch事件被觸發的時候,SW 已經被徹底激活了。

cache storage API 和 localStorage,IndexedDB 同樣是「同域」的。若是你在一個父域下運行多個網站,好比 yourname.github.io/myapp,這就要當心你不要把別的網站的緩存刪掉了。避免這個問題,你能夠將 cache 的 key 設的具備惟一性,好比 myapp-static-v1 而且約束不要碰不以 myapp- 開頭的緩存。

skipWaiting

waiting 意在讓你的網站同一時間只有一個 SW 在運行,但若是你不想要這樣的話,你能夠經過調用self.skipWaiting()來讓新 SW 當即激活。

這麼作會讓你的新 SW 踢掉舊的,而後當它變爲 waiting 狀態時當即激活,注意這裏不會跳過 installing,只會跳過 waiting。

在 waiting 以前或者以後調用skipWaiting()均可以,通常狀況咱們在 install 事件中調用:

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

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

這個例子中,你可能直接能夠看到一隻奶牛,和clients.claim()同樣,這是一場賽跑,僅當你的新 SW 安裝,激活等早於你請求圖片時,奶牛纔會出現。

skipWaiting()意味着新 SW 控制了以前用舊 SW 獲取的頁面,也就是說你的頁面有一部分資源是經過舊 SW 獲取,剩下一部分是經過新 SW 獲取的,若是這樣作會給你帶來麻煩,那就不要用skipWaiting(),這點咱們應該根據具體狀況評估。

手動更新

像我以前說的,當頁面刷新或者執行功能性事件時,瀏覽器會自動檢查更新,其實咱們也能夠手動的來觸發更新:

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

若是你但願你的用戶訪問頁面很長時間並且不用刷新,那麼你能夠每一個一段時間調用一次update()

避免改變 SW 的 URL

若是你看過個人文章緩存最佳實踐,你可能會考慮給每一個 SW 不一樣的 URL。千萬不要這麼作!在 SW 中這麼作是「最差實踐」,要在原地址上修改 SW。

舉個例子來講明爲何:

1.index.html註冊了sw-v1.js做爲SW。

2.sw-v1.jsindex.html作了緩存,也就是緩存優先(offline-first)。

3.你更新了index.html從新註冊了在新地址的 SW sw-v2.js.

若是你像上面那麼作,用戶永遠也拿不到sw-v2.js,由於index.htmlsw-v1.js緩存中,這樣的話,若是你想更新爲sw-v2.js,還須要更改原來的sw-v1.js

在上面的 demo 裏,我給每一個 SW 用了不一樣的 URL,這只是爲了作演示,不要在生產環境中這麼作。

讓開發更簡單

SW 的生命週期是爲了用戶構建的,但這樣不免讓咱們開發帶來一些煩惱,幸好與一些工具來幫助咱們。

Update on reload

這是我最喜歡的:

alt

這樣把生命週期變得對開發友好了,每次跳轉將會:

1.從新獲取 SW

2.儘管字節一致,也會從新安裝,也就是說install事件被執行而且更新緩存。

3.跳過 waiting,激活新的 SW。

4.導航到這個頁面。

這就是說你每次操做都會更新而不用刷新頁面或者關閉 tab。

Skip Waiting

alt
若是你有個 SW 在等待狀態,你能夠點擊 skipWaiting 讓它當即變爲激活狀態。

強制刷新

若是你強制刷新頁面,那麼會繞過 SW,變成不受控,這個功能已被定爲規範,因此在其餘支持 SW 的瀏覽器中也適用。

處理更新

Service Worker 是 可擴展web 的一部分。想法初衷是,做爲瀏覽器開發者,有時候咱們可能不如 web 開發者更瞭解 web,因此,咱們其實不該該提供僅僅能夠解決具體問題的 API,而是應該給 web 開發者更多的權限從而更好的解決問題。

因此,咱們儘量的開放更多,SW 整個生命週期都是可查看的:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // 安裝中的 SW,或者是undefined
  reg.waiting; // 等待中的 SW,或者是undefined
  reg.active; // 激活中的 SW,或者是undefined

  reg.addEventListener('updatefound', () => {
    // 正在安裝的新的 SW
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - 安裝事件被觸發,但還沒完成
    // "installed"  - 安裝完成
    // "activating" - 激活事件被觸發,但還沒完成
    // "activated"  - 激活成功
    // "redundant"  - 廢棄,多是由於安裝失敗,或者是被一個新版本覆蓋

    newWorker.addEventListener('statechange', () => {
      // newWorker 狀態發生變化
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
    // 當 SW controlling 變化時被觸發,好比新的 SW skippedWaiting 成爲一個新的被激活的 SW
});
相關文章
相關標籤/搜索