工作箱視窗

workbox-window 套件是一組模組,可在 window 結構定義中執行 (也就是在您的網頁中)。這些元件可補充在 Service Worker 中執行的其他 Workbox 套件。

workbox-window 的主要功能/目標為:

匯入及使用工作方塊視窗

workbox-window 套件的主要進入點為 Workbox 類別,您可以從我們的 CDN 或使用任何熱門的 JavaScript 組合工具匯入您的程式碼。

使用我們的 CDN

Workbox 類別匯入網站最簡單的方法,就是從我們的 CDN 匯入:

<script type="module">
  import {Workbox} from 'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-window.prod.mjs';

  if ('serviceWorker' in navigator) {
    const wb = new Workbox('/sw.js');

    wb.register();
  }
</script>

請注意,這個範例使用 <script type="module">import 陳述式載入 Workbox 類別。雖然您可能認為必須轉譯這個程式碼,才能在舊版瀏覽器中運作,但其實並非必要。

支援 Service Worker 的所有主要瀏覽器也支援原生 JavaScript 模組,因此非常適合將這段程式碼提供給任何瀏覽器 (舊版瀏覽器只會忽略這項程式碼)。

使用 JavaScript 套件載入 Workbox

雖然使用 workbox-window 絕對不需要工具,但如果您的開發基礎架構已包含可與 npm 依附元件搭配使用的 webpackRollup 等整合工具,則可使用這些項目載入 workbox-window

第一步是安裝 workbox-window 做為應用程式的依附元件:

npm install workbox-window

接著,在應用程式的其中一個 JavaScript 檔案中參照 workbox-window 套件名稱,以 import 工作箱:

import {Workbox} from 'workbox-window';

if ('serviceWorker' in navigator) {
  const wb = new Workbox('/sw.js');

  wb.register();
}

如果您的套裝組合支援透過動態匯入陳述式進行程式碼分割,您也可以有條件載入 workbox-window,這有助於縮減頁面主要套件的大小。

即使 workbox-window 很小,也不需要將其與網站的核心應用程式邏輯一起載入,因為服務工作站的本質就是漸進式的改進。

if ('serviceWorker' in navigator) {
  const {Workbox} = await import('workbox-window');

  const wb = new Workbox('/sw.js');
  wb.register();
}

進階組合概念

與在 Service Worker 中執行的 Workbox 套件不同,workbox-windowmainmodule 欄位參照的建構檔案會轉譯為 ES5。package.json這使得程式庫與今日的建構工具相容,其中部分工具不允許開發人員轉換任何 node_module 依附元件。

如果您的建構系統「確實」允許您轉譯依附元件 (或者不需要轉譯任何程式碼),建議您匯入特定來源檔案,而不要匯入套件本身。

以下是匯入 Workbox 的各種方式,以及各傳回內容的說明:

// Imports a UMD version with ES5 syntax
// (pkg.main: "build/workbox-window.prod.umd.js")
const {Workbox} = require('workbox-window');

// Imports the module version with ES5 syntax
// (pkg.module: "build/workbox-window.prod.es5.mjs")
import {Workbox} from 'workbox-window';

// Imports the module source file with ES2015+ syntax
import {Workbox} from 'workbox-window/Workbox.mjs';

示例

匯入 Workbox 類別後,即可用該類別註冊,並與服務工作站互動。以下列舉一些可以在應用程式中使用 Workbox 的方法:

註冊 Service Worker,並在第一次使用 Service Worker 時通知使用者

許多網頁應用程式使用者服務工作站會預先快取資產,以便在後續載入頁面時離線運作。在某些情況下,建議您告知使用者應用程式現在已可離線使用。

const wb = new Workbox('/sw.js');

wb.addEventListener('activated', event => {
  // `event.isUpdate` will be true if another version of the service
  // worker was controlling the page when this version was registered.
  if (!event.isUpdate) {
    console.log('Service worker activated for the first time!');

    // If your service worker is configured to precache assets, those
    // assets should all be available now.
  }
});

// Register the service worker after event listeners have been added.
wb.register();

如果服務工作處理程序已安裝完畢,但無法順利啟動,請通知使用者

根據預設,當現有服務工作站控制的頁面註冊新的服務工作站時,在初始服務工作站控管的所有用戶端都完全卸載之前,Service Worker 才會啟動。

這可能會讓開發人員感到困惑,尤其是重新載入目前頁面未導致新 Service Worker 啟用的情況下。

為盡量避免混淆,並讓使用者清楚瞭解情況發生,Workbox 類別提供您可以監聽的 waiting 事件:

const wb = new Workbox('/sw.js');

wb.addEventListener('waiting', event => {
  console.log(
    `A new service worker has installed, but it can't activate` +
      `until all tabs running the current version have fully unloaded.`
  );
});

// Register the service worker after event listeners have been added.
wb.register();

通知使用者 workbox-broadcast-update 套件的快取更新

workbox-broadcast-update 套件是從快取提供內容 (快速交付) 的絕佳方式,也能使用過時/重新驗證策略通知使用者更新內容。

如要從視窗接收這些更新,您可以監聽 CACHE_UPDATED 類型的 message 事件:

const wb = new Workbox('/sw.js');

wb.addEventListener('message', event => {
  if (event.data.type === 'CACHE_UPDATED') {
    const {updatedURL} = event.data.payload;

    console.log(`A newer version of ${updatedURL} is available!`);
  }
});

// Register the service worker after event listeners have been added.
wb.register();

將要快取的網址清單傳送給服務工作人員

在某些應用程式中,您可知道在建構期間需要預先快取的所有素材資源,但有些應用程式會根據使用者最初到達的網址,提供完全不同的網頁。

針對後者類別的應用程式,建議您只快取使用者造訪的特定網頁所需的資產。使用 workbox-routing 套件時,您可向路由器傳送要快取的網址清單,系統會根據路由器本身定義的規則快取這些網址。

這個範例會在啟用新的 Service Worker 時,將網頁載入的網址清單傳送至路由器。請注意,您可以傳送「所有」網址,因為系統只會快取與 Service Worker 中已定義路徑相符的網址:

const wb = new Workbox('/sw.js');

wb.addEventListener('activated', event => {
  // Get the current page URL + all resources the page loaded.
  const urlsToCache = [
    location.href,
    ...performance.getEntriesByType('resource').map(r => r.name),
  ];
  // Send that list of URLs to your router in the service worker.
  wb.messageSW({
    type: 'CACHE_URLS',
    payload: {urlsToCache},
  });
});

// Register the service worker after event listeners have been added.
wb.register();

重要服務工作站生命週期時刻

服務工作站生命週期相當複雜,難以完整理解。這個流程非常複雜,其中一項原因是服務工作站的所有可能使用情況都必須處理所有邊緣案例 (例如註冊多個 Service Worker、在不同影格中註冊不同的 Service Worker、使用不同的名稱註冊 Service Worker 等)。

但大多數實作 Service Worker 的開發人員都不需要擔心所有這些邊緣案例,因為它們的用法相當簡單。大部分的開發人員每次載入網頁時只會註冊一個 Service Worker,且不會變更部署至伺服器的 Service Worker 名稱

Workbox 類別會將所有服務工作站註冊資料分為兩種類別,分別是執行個體自有、已註冊的 Service Worker 和外部 Service Worker,因此更適合用於服務工作站生命週期的簡易檢視畫面:

  • 已註冊的 Service Worker:在註冊時呼叫 register() 時,因 Workbox 執行個體呼叫 register() 或已經啟用的 Service Worker 而開始安裝的 Service Worker。updatefound
  • 外部 Service Worker:獨立開始安裝呼叫 register()Workbox 例項的 Service Worker。這種情況,通常是因為使用者在另一個分頁中開啟了網站的新版本。如果事件源自外部 Service Worker,則事件的 isExternal 屬性會設為 true

在瞭解這兩種服務工作處理程序後,以下將詳細介紹所有重要的服務工作處理程序生命週期時刻,以及開發人員建議如何處理這些事件:

第一次安裝 Service Worker

您可能需要將服務工作站首次安裝與日後所有更新的方式不同。

workbox-window 中,您可以檢查下列任一事件的 isUpdate 屬性,區分版本首次安裝和日後更新。如果是第一次安裝,isUpdate 會是 false

const wb = new Workbox('/sw.js');

wb.addEventListener('installed', event => {
  if (!event.isUpdate) {
    // First-installed code goes here...
  }
});

wb.register();
重要時刻 活動 建議採取的行動
已安裝新的 Service Worker (首次) installed

服務工作處理程序第一次安裝時,通常會預先快取網站離線工作所需的所有資產。建議您告知使用者,網站現在可離線運作。

此外,由於 Service Worker 首次安裝該軟體,不會對該頁面載入作業攔截擷取事件,建議您同時快取已載入的資產 (但如果這些資產已預先快取,就不需要這麼做)。上方的「將網址清單傳送給服務工作站」範例會說明如何執行這項作業。

Service Worker 已開始控管網頁 controlling

安裝新的 Service Worker 並開始控制頁面後,所有後續的擷取事件都會通過該 Service Worker。如果服務工作站新增了處理特定擷取事件的特殊邏輯,這就是您知道該邏輯將執行的時機。

請注意,首次安裝 Service Worker 時,除非服務工作站在啟用事件中呼叫 clients.claim(),否則「不會」開始控制目前的頁面。預設行為是等到下一個網頁載入完成後再開始控制。

workbox-window 的角度來看,這表示只有在服務工作站呼叫 clients.claim() 的情況下,才會分派 controlling 事件。如果網頁在註冊前已受到控制,系統就不會傳送這個事件。

Service Worker 已完成啟用 activated

如上所述,服務工作站首次完成啟用時,可能不會開始控制頁面。

因此,您不應透過監聽啟用事件的方式,判斷 Service Worker 何時控制該頁面。但是,如果您在進行中的事件 (在 Service Worker) 中執行邏輯,且需要知道相關邏輯何時完成,已啟用的事件會顯示這項資訊。

找到 Service Worker 的更新版本時

當新的 Service Worker 開始安裝,但現有版本正在控制該頁面時,下列所有事件的 isUpdate 屬性將為 true

這種情況與首次安裝時的回應方式通常和首次安裝不同,因為您必須管理使用者取得此更新的時間和方式。

重要時刻 活動 建議採取的行動
已安裝新的 Service Worker (更新先前的服務) installed

如果這不是第一次安裝 Service Worker (event.isUpdate === true),表示系統已找到並安裝新版 Service Worker (也就是與目前控制該網頁的版本不同版本)。

這通常表示伺服器已部署新版的網站,而且新的資產可能剛剛已完成預先快取。

注意:部分開發人員會使用 installed 事件,通知使用者網站有新版本。不過,視您是否已在安裝的 Service Worker 中呼叫 skipWaiting(),已安裝的 Service Worker 不一定會立即啟用。如果您「確實」呼叫了 skipWaiting(),建議您在新 Service Worker 啟用後通知使用者更新;如果您「並未」呼叫 skipWaiting,則最好通知對方等待事件中待處理的更新 (詳情請參閱下方說明)。

Service Worker 已安裝完畢,但一直停留在等候階段 waiting

如果 Service Worker 的更新版本在安裝期間並未呼叫 skipWaiting(),則系統會等到目前使用中 Service Worker 控管的所有頁面都卸載之後,才會啟用該版本。建議您通知使用者有可用的更新,並在下次造訪時套用。

警告!開發人員通常會提示使用者重新載入應用程式以取得更新,但在許多情況下, 重新整理頁面並不會啟動已安裝的工作站。如果使用者重新整理頁面,且服務工作站「還在」等候,waiting 事件就會再次觸發,且 event.wasWaitingBeforeRegister 屬性為 true。請注意,我們計畫在日後推出的版本中改善這項服務。請參閱問題 #1848,取得最新消息。

另一個選項是提示使用者並詢問他們是否要更新,還是繼續等待。如果選擇取得更新,您可以使用 postMessage() 要求 Service Worker 執行 skipWaiting()。如需相關範例,請參閱進階食譜 為使用者提供頁面重新載入功能

Service Worker 已開始控管網頁 controlling

更新後的 Service Worker 開始控制頁面時,表示目前控制的 Service Worker 版本與頁面載入時控制的版本不同。在某些情況下,可能沒問題,但也代表目前網頁參照的部分資產已不在快取中 (也可能不在伺服器上),您可以考慮告知使用者,網頁的某些部分可能無法正常運作。

注意:如果您未在 Service Worker 中呼叫 skipWaiting(),則 controlling 事件不會觸發。

Service Worker 已完成啟用 activated 更新的 Service Worker 啟用完畢後,表示您在服務工作站 activate 中執行的所有邏輯已完成。如果需要延後任何事項,直到這個邏輯結束為止,那麼這就是執行的時間。

發現未預期的 Service Worker 版本時

有時使用者會在背景分頁中長時間開啟您的網站。他們甚至可能開啟新分頁並瀏覽您的網站,卻不知道他們已在背景分頁中開啟您的網站。在這種情況下,您的網站有兩種版本可能會同時執行,而且開發人員可能會遇到一些有趣的問題。

假設在網站 A 執行 v1 與執行 v2 的分頁 B,載入分頁 B 時,將由隨附 v1 的服務工作站版本控制,但伺服器傳回的頁面 (如果您的導覽要求使用網路優先快取策略) 會包含您所有的 v2 資產。

但分頁 B 通常不會造成問題,因為當您編寫第 2 版程式碼時,已瞭解 v1 程式碼的運作方式。不過,這對分頁 A 可能有問題,因為您的 v1 程式碼無法預測變更 v2 程式碼可能引入的變更。

為協助處理這類情況,workbox-window 也會在偵測到來自「外部」服務工作站的更新時,分派生命週期事件,其中外部僅代表任何版本並非目前註冊的 Workbox 執行個體版本。

自 Workbox 6 以上版本起,這些事件與上述事件相等,並會在每個事件物件上新增 isExternal: true 屬性。如果網頁應用程式需要實作特定邏輯來處理「外部」服務工作站,您可以在事件處理常式中查看該屬性。

避免常見錯誤

Workbox 最實用的功能之一就是開發人員記錄。這對 workbox-window 尤其重要。

我們瞭解使用 Service Worker 進行開發時經常令人感到困惑,如果情況與您預期的不同,我們可能難以理解原因。

例如,當您變更服務工作站並重新載入頁面時,您可能不會在瀏覽器中看到這項變更。最有可能的原因是您的服務工作站仍在等待啟動。

不過,當您使用 Workbox 類別註冊 Service Worker 時,您將會收到開發人員主控台中的所有生命週期狀態變更相關資訊,這應該有助於偵錯問題未如預期的原因。

顯示正在等候工作站的 Workbox-Window 控制台警告

此外,開發人員第一次使用 Service Worker 時,經常會犯錯的錯誤,就是在錯誤範圍中註冊 Service Worker。

為了避免發生這種情形,如果註冊 Service Worker 的網頁不在該 Service Worker 的範圍內,Workbox 類別會顯示警告。如果服務工作站已啟用,但尚未控制頁面,系統也會發出警告:

非控制工作站的 Workbox 視窗控制台警告

Service Worker 通訊視窗

大部分的進階服務工作站用量牽涉到 Service Worker 和視窗之間傳送大量訊息。Workbox 類別也能提供 messageSW() 方法,這會對執行個體已註冊的 Service Worker 執行 postMessage() 並等待回應,以助您一臂之力。

雖然您可以採用任何格式將資料傳送至 Service Worker,但所有 Workbox 套件共用的格式都是包含三個屬性的物件 (後者是選用屬性):

屬性 必填與否 類型 說明
type string

不重複的字串,用於識別此訊息。

按照慣例,類型全部為大寫,並加上底線分隔字詞。如果類型代表要採取的行動,則該類型應為即時型態的指令 (例如 CACHE_URLS)。如果類型代表回報的資訊,則應採用過去時態 (例如 URLS_CACHED)。

meta no string 在 Workbox 中,這一律是傳送訊息的 Workbox 套件名稱。自行傳送訊息時,您可以略過這個屬性,或是視需要設定屬性。
payload no * 傳送的資料。這通常是物件,但其實不然。

透過 messageSW() 方法傳送的訊息會使用 MessageChannel,因此接收器才能回應訊息。如要回覆訊息,您可以在訊息事件監聽器中呼叫 event.ports[0].postMessage(response)messageSW() 方法會傳回承諾,該承諾會解析為您回覆的任何 response

以下示範如何將訊息從視窗傳送至 Service Worker,並取得回應。第一個程式碼區塊是服務工作站中的訊息事件監聽器,第二個區塊則使用 Workbox 類別傳送訊息並等候回應:

sw.js 中的程式碼:

const SW_VERSION = '1.0.0';

addEventListener('message', event => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

main.js 中的程式碼 (在視窗中執行):

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

管理版本不相容的情況

以上範例示範如何透過視窗檢查 Service Worker 版本。這個範例的用意是,因為當您在視窗與 Service Worker 之間來回傳送訊息時,請務必注意,服務工作站執行的網站版本可能與執行網頁程式碼的版本不同,而解決這個問題的解決方案則因網頁優先服務或快取優先。

先連上網路

先提供網頁網路時,使用者一律會從您的伺服器取得最新版本的 HTML。不過,使用者初次造訪網站 (部署更新後) 時,使用者收到的 HTML 將是最新版本,但在瀏覽器中執行的 Service Worker 會是之前安裝的版本 (可能是許多舊版本)。

請務必瞭解這種可能性,因為如果網頁目前版本載入的 JavaScript 會將訊息傳送至舊版的 Service Worker,可能就無法知道該版本的回應方式,或是可能會以不相容的格式回應。

因此,建議您在執行任何重要工作前,一律為 Service Worker 執行版本版本檢查,並檢查是否有相容版本。

例如,在上述程式碼中,如果 messageSW() 呼叫傳回的 Service Worker 版本比預期版本更舊,建議等到找到更新 (在您呼叫 register() 時會發生) 為止。屆時,您可以通知使用者或更新該階段,也可以手動略過等候階段,立即啟用新的 Service Worker。

先快取

不同於以網路為優先提供網頁快取的情況,在優先提供網頁快取時,您會知道自己的網頁最初永遠會與服務工作站的版本相同 (因為用到的是服務工作站)。因此,您可以放心立即使用 messageSW()

不過,如果系統在網頁呼叫 register() 時找到並啟動 Service Worker 的更新版本 (也就是您刻意略過等候階段),則傳送訊息到工作站可能就沒有問題。

管理這種可能性的策略之一,是使用版本管理配置,可區分破壞性更新和非破壞性更新;而在破壞性更新的情況下,您知道傳送訊息給 Service Worker 並不安全。請改為警告使用者正在執行舊版頁面,建議使用者重新載入頁面,取得更新。

略過等待輔助程式

視窗對服務工作站訊息傳遞作業的常見使用慣例是傳送 {type: 'SKIP_WAITING'} 訊息,指示已安裝的服務工作站略過等候階段並啟用。

自 Workbox v6 起,messageSkipWaiting() 方法可用來傳送 {type: 'SKIP_WAITING'} 訊息給與目前 Service Worker 註冊相關聯的等候服務工作站。如果沒有等待 Service Worker,則不會採取任何行動。

類型

Workbox

可協助處理 Service Worker 註冊、更新,以及對服務工作站生命週期事件做出回應的類別。

屬性

  • 建構函式

    void

    建立具有指令碼網址和服務工作站選項的新 Workbox 執行個體。指令碼網址和選項與呼叫 navigator.serviceWorker.register(scriptURL, options) 時使用的指令碼網址和選項相同。

    constructor 函式如下所示:

    (scriptURL: string|TrustedScriptURL,registerOptions?: object)=> {...}

    • scriptURL

      string|TrustedScriptURL

      與這個執行個體相關聯的 Service Worker 指令碼。系統支援使用 TrustedScriptURL

    • registerOptions

      物件選用

  • 已啟用

    Promise<ServiceWorker>

  • 控制

    Promise<ServiceWorker>

  • getSW

    void

    可供使用的 Service Worker 參照完成解析,該參照與這個執行個體的指令碼網址相符。

    如果在註冊時已有有效或等待中的服務工作站具備相符的指令碼網址,系統就會使用該工作站 (如果兩者相符,則等待服務工作站優先於執行中的服務工作站,因為等待中的服務工作站最近註冊的時間較高)。如果註冊時沒有相符的有效或等待中的 Service Worker,則必須等到找到更新並開始安裝後,才會解析承諾項目,屆時系統會使用安裝的 Service Worker。

    getSW 函式如下所示:

    ()=> {...}

    • returns

      Promise<ServiceWorker>

  • messageSW

    void

    將傳遞的資料物件 (透過 workbox-window.Workbox#getSW) 傳送至由此執行個體註冊的 Service Worker,並用回應 (如果有的話) 解析。

    呼叫 event.ports[0].postMessage(...) 即可在服務工作站的訊息處理常式中設定回應,以解決 messageSW() 傳回的承諾。如未設定回應,承諾永遠無法解決。

    messageSW 函式如下所示:

    (data: object)=> {...}

    • 資料或曾存取這類資料的人員

      物件

      要傳送至 Service Worker 的物件

    • returns

      承諾<任何>

  • messageSkipWaiting

    void

    傳送 {type: 'SKIP_WAITING'} 訊息給目前處於與目前註冊相關聯的 waiting 狀態的 Service Worker。

    如果目前沒有註冊狀態,或沒有任何 Service Worker 是 waiting,則呼叫此方法不會有任何作用。

    messageSkipWaiting 函式如下所示:

    ()=> {...}

  • register

    void

    為這個執行個體指令碼網址和服務工作站選項註冊服務工作站。根據預設,這個方法會延遲註冊,直到視窗載入後才會執行。

    register 函式如下所示:

    (options?: object)=> {...}

    • 選項

      物件選用

      • 立即

        布林值 (選用)

    • returns

      Promise<ServiceWorkerRegistration>

  • update

    void

    檢查已註冊的 Service Worker 更新。

    update 函式如下所示:

    ()=> {...}

    • returns

      Promise<void>

WorkboxEventMap

WorkboxLifecycleEvent

屬性

  • isExternal

    布林值 (選用)

  • isUpdate

    布林值 (選用)

  • originalEvent

    活動 選用

  • sw

    ServiceWorker (選用)

  • 目標

    WorkboxEventTarget 選用

  • 類型

    typeOperator

WorkboxLifecycleEventMap

WorkboxLifecycleWaitingEvent

屬性

  • isExternal

    布林值 (選用)

  • isUpdate

    布林值 (選用)

  • originalEvent

    活動 選用

  • sw

    ServiceWorker (選用)

  • 目標

    WorkboxEventTarget 選用

  • 類型

    typeOperator

  • wasWaitingBeforeRegister

    布林值 (選用)

WorkboxMessageEvent

屬性

  • 資料或曾存取這類資料的人員

    不限

  • isExternal

    布林值 (選用)

  • originalEvent

    活動

  • ports

    typeOperator

  • sw

    ServiceWorker (選用)

  • 目標

    WorkboxEventTarget 選用

  • 類型

方法

messageSW()

workbox-window.messageSW(
  sw: ServiceWorker,
  data: object,
)

透過 postMessage 將資料物件傳送至服務工作站,並透過回應解析 (如果有的話)。

呼叫 event.ports[0].postMessage(...) 即可在服務工作站的訊息處理常式中設定回應,以解決 messageSW() 傳回的承諾。如未設定回應,承諾就無法解決。

參數

  • sw

    ServiceWorker

    要接收訊息的 Service Worker。

  • 資料或曾存取這類資料的人員

    物件

    要傳送至 Service Worker 的物件。

傳回

  • 承諾<任何>