工作框窗口

workbox-window 软件包是将在 window 上下文(也就是说,在您的网页内部)中运行的一组模块。它们是对在 Service Worker 中运行的其他 Workbox 软件包的补充。

workbox-window的主要功能/目标包括:

导入并使用 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();
}

如果您的捆绑器支持通过动态 import 语句拆分代码,您还可以有条件地加载 workbox-window,这有助于缩减页面主 bundle 的大小。

虽然 workbox-window 非常小,但由于 Service Worker 本质上属于渐进式增强功能,因此没有理由需要它与网站的核心应用逻辑一起加载。

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

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

高级邮件分类概念

与在 Service Worker 中运行的 Workbox 软件包不同,package.jsonworkbox-windowmainmodule 字段引用的 build 文件将转译为 ES5。这使其与当前的构建工具兼容,其中一些构建工具不允许开发者转译其 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 类后,您可以使用它来注册 Service Worker 并与之交互。以下是在应用中使用 Workbox 的一些方式示例:

注册一个 Service Worker,并在 Service Worker 首次处于活动状态时通知用户

许多 Web 应用都使用 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 控制的页面注册新的 Service Worker 时,默认情况下,直到初始 Service Worker 控制的所有客户端完全卸载后,该 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 软件包是一种能够从缓存传送内容(用于快速传送)的好方法,同时还能够通知用户相应内容的更新(使用 stale-while-revalidate 策略)。

如需从该窗口接收这些更新,您可以监听 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();

向 Service Worker 发送要缓存的网址列表

对于某些应用,可能知道构建时需要预缓存的所有资源,但有些应用会根据用户最先到达的网址提供完全不同的页面。

对于后一类别的应用,可能合理的做法是,仅缓存用户访问过的特定网页所需的资源。使用 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,等等)。

但是,大多数实现 Service Worker 的开发者都不需要担心所有这些边缘情况,因为其使用方法非常简单。大多数开发者在每次网页加载时只注册一个 Service Worker,并且他们不会更改他们部署到服务器的 Service Worker 的名称

Workbox 类采用这种更简单的 Service Worker 生命周期视图,它将所有 Service Worker 注册分为两类:实例自有的已注册 Service Worker 和外部 Service Worker:

  • 已注册的 Service Worker:由于 Workbox 实例调用 register() 或已处于活动状态的 Service Worker(如果调用 register() 没有在注册时触发 updatefound 事件)而开始安装的 Service Worker。
  • 外部 Service Worker:开始独立于 Workbox 实例安装的 Service Worker,调用 register()。当用户在另一个标签页中打开您网站的新版本时,通常会出现这种情况。当事件源自外部 Service Worker 时,事件的 isExternal 属性将设置为 true

了解了这两种类型的 Service Worker,下面详细介绍了所有重要的 Service Worker 生命周期时刻,以及开发者处理这些时刻的建议:

首次安装 Service Worker 时

您可能希望以不同的方式处理 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 在首次安装时不会拦截该网页加载的提取事件,因此您还可以考虑缓存已加载的资源(但如果这些资源已经预缓存,就不需要这样做)。上面的向 Service Worker 发送要缓存的网址列表示例显示了如何执行此操作。

Service Worker 已开始控制页面 controlling

在新 Service Worker 安装并开始控制页面后,所有后续提取事件都将通过该 Service Worker。如果您的 Service Worker 添加任何特殊逻辑来处理特定提取事件,那么您就会知道该逻辑将会运行。

请注意,当您第一次安装 Service Worker 时,它不会开始控制当前页面,除非该 Service Worker 在其激活事件中调用 clients.claim()默认行为是等到下一个页面加载才开始控制。

workbox-window 的角度来看,这意味着只有在 Service Worker 调用 clients.claim() 的情况下才会分派 controlling 事件。如果网页在注册之前已受到控制,则不会分派此事件。

Service Worker 已完成激活 activated

如上所述,Service Worker 首次完成激活后可能(也可能不会)已经开始控制页面。

因此,您不应通过监听激活事件来了解 Service Worker 何时控制着页面。不过,如果您在活跃事件(在 Service Worker 中)中运行逻辑,并且您需要知道该逻辑何时完成,则激活事件会通知您。

发现 Service Worker 的更新版本时

当新的 Service Worker 开始安装但现有版本当前正在控制页面时,以下所有事件的 isUpdate 属性将为 true

您在这种情况下的反应通常与首次安装不同,因为您必须管理用户获取此更新的时间和方式。

重要时刻 事件 建议采取的措施
安装了新的 Service Worker(更新之前的 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 控制的所有页面均已卸载后,它才会激活。您可能需要通知用户有可用更新,系统会在用户下次访问时应用更新。

警告!开发者通常会提示用户重新加载以获取更新,但在许多情况下, 刷新页面并不会激活已安装的 worker。如果用户刷新页面且 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 完成激活时,这意味着您在 Service Worker 的 activate 中运行的任何逻辑均已完成。如果您需要将任何内容延迟到该逻辑完成,此时就是运行该逻辑的时候。

发现意外的 Service Worker 版本时

有时,用户会长时间在后台标签页中打开您的网站。他们甚至可能会打开新的标签页并导航到您的网站,而没有意识到已经在后台标签页中打开了您的网站。在这种情况下,可能会出现同时运行网站的两个版本的情况,这可能会给开发者带来一些有趣的问题。

设想一个场景,其中标签页 A 运行网站的 v1 版本,标签页 B 运行 v2 版本。当标签页 B 加载时,它由随 v1 一起提供的 Service Worker 的版本控制,但是服务器返回的页面(如果针对导航请求使用网络优先缓存策略)将包含您的所有 v2 资源。

不过,对于标签页 B 而言,这通常不是问题,因为在编写 v2 代码时,您已经知道 v1 代码的运作方式。不过,这对标签页 A 来说可能是问题,因为您的 v1 代码可能无法预测出 v2 代码可能会引入的更改。

为帮助处理这些情况,workbox-window 还会在检测到来自“外部”Service Worker 的更新时分派生命周期事件,其中“外部”仅表示不是当前 Workbox 实例注册的任何版本。

在 Workbox v6 及更高版本中,这些事件等同于上文所述的事件,只是在每个事件对象上添加了一个 isExternal: true 属性。如果您的 Web 应用需要实现特定逻辑来处理“外部”Service Worker,您可以在事件处理脚本中检查该属性。

避免常见错误

Workbox 提供的最有用的功能之一是开发者日志记录。workbox-window 尤其如此。

我们知道,使用 Service Worker 进行开发经常令人困惑,当出现与预期相反的情况时,很难知道原因是什么。

例如,当您对 Service Worker 进行更改并重新加载页面时,可能不会在浏览器中看到该更改。最可能的原因是您的 Service Worker 仍在等待激活。

但是,向 Workbox 类注册 Service Worker 时,您会在开发者控制台中收到所有生命周期状态变化的通知,这有助于调试出现异常情况的原因。

等待工作器的 Workbox-window 控制台警告

此外,开发者在首次使用 Service Worker 时常犯的一个错误是在错误作用域中注册 Service Worker。

为了防止发生这种情况,如果注册 Service Worker 的页面不在该 Service Worker 的范围内,Workbox 类将会向您发出警告。如果您的 Service Worker 处于活跃状态但尚未控制页面,它也会向您发出警告:

针对非控制 worker 的 Workbox-window 控制台警告

Service Worker 的通信窗口

大多数高级 Service Worker 的使用都涉及在 Service Worker 和窗口之间执行大量的消息传递。Workbox 类也通过提供 messageSW() 方法提供帮助,该方法将对实例注册的 Service Worker 执行 postMessage() 操作并等待响应。

虽然您可以通过任何格式向 Service Worker 发送数据,但所有 Workbox 软件包共享的格式都是一个具有以下三个属性的对象(后两个属性是可选的):

媒体资源 是否必需? 类型 说明
type string

用于标识消息的唯一字符串。

按照惯例,类型全部采用大写形式,用下划线将单词分隔开。如果一个类型表示要采取的操作,则应是一个采用当前时态的命令(例如 CACHE_URLS);如果类型表示正在报告的信息,则应使用过去式(例如 URLS_CACHED)。

meta string 在 Workbox 中,这始终是发送消息的 Workbox 软件包的名称。自行发送消息时,您可以省略此属性,也可以将其设为自己喜欢的任何值。
payload * 要发送的数据。通常,这是一个对象,但并非必须如此。

通过 messageSW() 方法发送的消息使用 MessageChannel,以便接收者可以对其进行响应。如需响应消息,您可以在消息事件监听器中调用 event.ports[0].postMessage(response)messageSW() 方法会返回一个 promise,它可以解析为您所回复的任何 response

以下是从窗口向 Service Worker 发送消息并获得响应的示例。第一个代码块是 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 之间来回发送消息时,请务必注意,您的 Service Worker 运行的网站版本可能与网页代码运行的版本不同,并且根据您是采用网络优先还是缓存优先方式处理网页,解决此问题的解决方案也有所不同。

网络优先

如果先投放您的网页网络,您的用户将始终从您的服务器获取最新版本的 HTML。然而,用户首次(部署更新后)再次访问您的网站时,他们获得的 HTML 将是最新版本,但其浏览器中运行的 Service Worker 将是先前安装的版本(可能是许多旧版本)。

了解这种可能性非常重要,因为如果当前版本的网页所加载的 JavaScript 向旧版 Service Worker 发送消息,该版本可能不知道如何响应(或者它可能以不兼容的格式响应)。

因此,在执行任何关键工作之前,最好始终对 Service Worker 进行版本控制,并检查是否存在兼容的版本。

例如,在上面的代码中,如果该 messageSW() 调用返回的 Service Worker 版本低于预期版本,则是明智的做法,即等待发现更新(当您调用 register() 时应会发生)。此时,您可以通知用户或有更新,也可以手动跳过等待阶段,立即激活新的 Service Worker。

缓存优先

与优先通过网络传送页面不同,在优先传送页面时,您知道页面最初始终将与 Service Worker 的版本相同(因为这是它提供的版本)。因此,您可以放心地立即使用 messageSW()

但是,如果在您的页面调用 register() 时找到并激活了 Service Worker 的更新版本(即您有意跳过等待阶段),向它发送消息可能不再安全。

降低这种可能性的一个策略是使用版本控制方案,让您能够区分破坏性更新和非破坏性更新,如果出现重大更新,您就会知道向 Service Worker 发送消息是不安全的。相反,您需要警告用户他们运行的是旧版网页,并建议他们重新加载以获取更新。

跳过等待帮助程序

窗口到 Service Worker 消息传递的常见惯例是发送 {type: 'SKIP_WAITING'} 消息,以指示安装的 Service Worker跳过等待阶段并激活。

从 Workbox v6 开始,messageSkipWaiting() 方法可用于向与当前 Service Worker 注册关联的等待 Service Worker 发送 {type: 'SKIP_WAITING'} 消息。如果没有等待的 Service Worker,它将不会执行任何操作,而且不会发出任何提示。

类型

Workbox

用于辅助处理 Service Worker 注册、更新和响应 Service Worker 生命周期事件的类。

属性

  • 构造函数

    void

    使用脚本网址和 Service Worker 选项创建新的 Workbox 实例。脚本网址和选项与调用 navigator.serviceWorker.register(script网址, options) 时使用的相同。

    constructor 函数如下所示:

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

    • scriptURL

      字符串|TrustedScript网址

      与此实例关联的 Service Worker 脚本。支持使用 TrustedScriptURL

    • registerOptions

      对象(可选)

  • 活跃

    Promise<ServiceWorker>

  • 正在控制

    Promise<ServiceWorker>

  • getSW

    void

    当此实例的脚本网址可用时,通过引用与此实例的脚本网址相匹配的 Service Worker 进行解析。

    如果在注册时已经存在具有匹配脚本网址的活跃或等待 Service Worker,则将使用它(如果两者都匹配,则等待的 Service Worker 优先于活跃的 Service Worker,因为等待的 Service Worker 应该是最近注册的)。如果在注册时没有匹配的活跃或等待 Service Worker,则 promise 将直到找到更新并开始安装,然后才会使用安装 Service Worker。

    getSW 函数如下所示:

    ()=> {...}

    • 返回

      Promise<ServiceWorker>

  • messageSW

    void

    将传递的数据对象发送到此实例注册的 Service Worker(通过 workbox-window.Workbox#getSW),并使用响应进行解析(如果有)。

    您可以通过调用 event.ports[0].postMessage(...) 在 Service Worker 的消息处理程序中设置响应,该方法将解析 messageSW() 返回的 promise。如果未设置响应,则 promise 将永远不会被解析。

    messageSW 函数如下所示:

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

    • data

      对象

      一个要发送给 Service Worker 的对象

    • 返回

      承诺<any>

  • messageSkipWaiting

    void

    向当前处于与当前注册关联的 waiting 状态的 Service Worker 发送 {type: 'SKIP_WAITING'} 消息。

    如果没有当前注册或没有 Service Worker 处于 waiting 状态,则调用该方法不会产生任何影响。

    messageSkipWaiting 函数如下所示:

    ()=> {...}

  • register

    void

    为此实例脚本网址和 Service Worker 选项注册一个 Service Worker。默认情况下,此方法会将注册延迟到窗口加载完毕之后。

    register 函数如下所示:

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

    • 选项

      对象(可选)

      • 当前位置

        布尔值 选填

    • 返回

      Promise<ServiceWorkerRegistration>

  • update

    void

    检查已注册的 Service Worker 的更新。

    update 函数如下所示:

    ()=> {...}

    • 返回

      Promise<void>

WorkboxEventMap

WorkboxLifecycleEvent

属性

  • isExternal

    布尔值 选填

  • isUpdate

    布尔值 选填

  • originalEvent

    活动可选

  • sw

    ServiceWorker 可选

  • 目标

    WorkboxEventTarget 可选

  • 类型

    typeOperator

WorkboxLifecycleEventMap

WorkboxLifecycleWaitingEvent

属性

  • isExternal

    布尔值 选填

  • isUpdate

    布尔值 选填

  • originalEvent

    活动可选

  • sw

    ServiceWorker 可选

  • 目标

    WorkboxEventTarget 可选

  • 类型

    typeOperator

  • wasWaitingBeforeRegister

    布尔值 选填

WorkboxMessageEvent

属性

  • data

    任意

  • isExternal

    布尔值 选填

  • originalEvent

    事件

  • ports

    typeOperator

  • sw

    ServiceWorker 可选

  • 目标

    WorkboxEventTarget 可选

  • 类型

方法

messageSW()

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

通过 postMessage 向 Service Worker 发送数据对象,并使用响应进行解析(如果有)。

您可以通过调用 event.ports[0].postMessage(...) 在 Service Worker 的消息处理程序中设置响应,该方法将解析 messageSW() 返回的 promise。如果未设置响应,则 promise 不会得到解析。

参数

  • sw

    ServiceWorker

    向其发送消息的 Service Worker。

  • data

    对象

    一个要发送到 Service Worker 的对象。

返回

  • 承诺<any>