「PWA」Workbox-Window v4.x 中文版

2,176 阅读18分钟

Workbox 目前发了一个大版本,从 v3.x 到了 v4.x,变化有挺大的,下面是在 window 环境下的模块。


什么是 workbox-window?

workbox-window 包是一组模块,用于在 window 上下文中运行,也就是说,在你的网页内部运行。 它们是 servicewoker 中运行的其他 workbox 的补充。

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

  • 通过帮助开发人员确定 serviceWorker 生命周期中最关键的时刻,并简化对这些时刻的响应,简化 serviceWoker 注册和更新的过程。
  • 帮助防止开发人员犯下最常见的错误。
  • 使 serviceWorker 程序中运行的代码与 window 中运行的代码之间的通信更加轻松。

导入和使用 workbox-window

workbox-window 包的主要入口点是 Workbox 类,你可以从我们的CDN或使用任何流行的 JavaScript 打包工具将其导入代码中。

使用我们的 CDN

在您的网站上导入 Workbox 类的最简单方法是从我们的 CDN:

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

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

  wb.register();
}
</script>

注意,此示例使用 <script type ="module">import 语句来加载 Workbox 类。 虽然您可能认为需要转换此代码以使其在旧版浏览器中运行,但实际上并不是必需的。

支持 serviceWorker 的所有主要浏览器也支持 JavaScript 模块,因此将此代码提供给任何浏览器都是完美的(旧版浏览器将忽略它)。

通过 JavScript 打包加载 Workbox

虽然使用 Workbox 绝对不需要工具,但如果您的开发基础架构已经包含了与 npm 依赖项一起使用的 webpackRollup 等打包工具,则可以使用它们来加载 Workbox

第一步就是安装 Workbox 做为你应用的依赖:

npm install workbox-window

然后,在您的某个应用程序的 JavaScript 文件中,通过引用 workbox-window 包名称导入 Workbox

import {Workbox} from 'workbox-window';

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

  wb.register();
}

如果您的打包工具支持通过动态 import 语句进行代码拆分,你还可以有条件地加载workbox-window,这有助于减少页面主包的大小。

尽管 Workbox 非常小(1kb gzip压缩),但是没有理由需要加载站点的核心应用程序逻辑,因为 serviceWorker 本质上是渐进式增强。

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

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

高级打包概念

与在 Service worker 中运行的 Workbox 包不同,workbox-windowpackage.json 中的 mainmodule 字段引用的构建文件被转换为 ES5。 这使它们与当今的构建工具兼容 - 其中一些不允许开发人员转换其 node_module 依赖项的任何内容。

如果你的构建系统允许您转换依赖项(或者如果您不需要转换任何代码),那么最好导入特定的源文件而不是包本身。

以下是你可以导入 Workbox 的各种方法,以及每个方法将返回的内容的说明:

// 使用ES5语法导入UMD版本
// (pkg.main: "build/workbox-window.prod.umd.js")
const {Workbox} = require('workbox-window');

// 使用ES5语法导入模块版本
// (pkg.module: "build/workbox-window.prod.es5.mjs")
import {Workbox} from 'workbox-window';

// 使用ES2015 +语法导入模块源文件
import {Workbox} from 'workbox-window/Workbox.mjs';

重要! 如果您直接导入源文件,则还需要配置构建过程以缩小文件,并在将其部署到生产时删除仅开发代码。 有关详细信息,请参阅使用打包(webpack / Rollup)和Workbox的指南

示例

导入 Workbox 类后,可以使用它来注册 serviceWorker 并与之交互。 以下是您可以在应用程序中使用 Workbox 的一些示例:

注册 serviceWorker 并在 serviceWorker 第一次处于 active 状态时通知用户:

许多 Web 应用程序用户 serviceWorker 预缓存资源,以便其应用程序在后续页面加载时离线工作。在某些情况下,通知用户该应用程序现在可以离线使用是有意义的。

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

wb.addEventListener('activated', (event) => {
  // 如果另一个版本的 serviceWorker,`event.isUpdate`将为true
  // 当这个版本注册时,worker 正在控制页面。
  if (!event.isUpdate) {
    console.log('Service worker 第一次激活!');

    // 如果您的 serviceWorker 配置为预缓存资源,那么
    // 资源现在都应该可用。
  }
});

// 添加事件侦听器后注册 serviceWorker 。
wb.register();

如果 serviceWorker 已安装但等待激活,则通知用户

当由现有 serviceWorker 控制的页面注册新的 serviceWorker 时,默认情况下,在初始 serviceWorker 控制的所有客户端完全卸载之前,serviceWorker 将不会激活。

这是开发人员常见的混淆源,特别是在重新加载当前页面不会导致新 serviceWorker 程序激活的情况下。

为了帮助减少混淆并在发生这种情况时明确说明,Workbox 类提供了一个可以监听的等待事件:

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

wb.addEventListener('waiting', (event) => {
  console.log(`已安装新的 serviceWorker,但无法激活` +
      `直到运行当前版本的所有选项卡都已完全卸载。`);
});

// 添加事件侦听器后注册 service worker 。
wb.register();

从 workbox-broadcast-update 包通知用户缓存更新

workbox-broadcast-update 包非常棒

能够从缓存中提供内容(快速交付)的方式,同时还能够通知用户该内容的更新(使用stale-while-revalidate 策略)。

要从 window 接收这些更新,您可以侦听 CACHE_UPDATE 类型的消息事件:

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

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

    console.log(`${updatedURL} 的更新版本可用`);
  }
});

// 添加事件侦听器后注册 service worker。
wb.register();

向 serviceWorker 发送要缓存的URL列表

对于某些应用程序,可以知道在构建时需要预先缓存的所有资源,但某些应用程序根据用户首先登陆的 URL 提供完全不同的页面。

对于后一类别的应用程序,仅缓存用户所访问的特定页面所需的资源可能是有意义的。 使用 workbox-routing 软件包时,您可以向路由器发送一个 URL 列表进行缓存,它将根据路由器本身定义的规则缓存这些 URL。

每当激活新的 serviceWorker 时,此示例都会将页面加载的 URL 列表发送到路由器。 请注意,发送所有 URL 是可以的,因为只会缓存与 serviceWorker 中定义的路由匹配的 URL:

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

wb.addEventListener('activated', (event) => {
  // 获取当前页面URL +页面加载的所有资源。
  const urlsToCache = [
    location.href,
    ...performance.getEntriesByType('resource').map((r) => r.name),
  ];
  // 将该URL列表发送到 serviceWorker 的路由器。
  wb.messageSW({
    type: 'CACHE_URLS',
    payload: {urlsToCache},
  });
});

// 添加事件侦听器后注册 serviceWorker。
wb.register();

注意:上述技术适用于通过默认路由器上的 workbox.routing.registerRoute() 方法定义的任何路由。 如果您要创建自己的路由器实例,则需要手动调用 addCacheListener() 。

重要的 serviceWorker 生命周期

serviceWorker 的生命周期很复杂,完全可以理解。 它之所以如此复杂,部分原因在于它必须处理 serviceWorker 所有可能使用的所有边缘情况(例如,注册多个 serviceWorker,在不同的框架中注册不同的 serviceWorker,注册具有不同名称的 serviceWorker 等)。

但是大多数实现 serviceWorker 的开发人员不应该担心所有这些边缘情况,因为它们的使用非常简单。 大多数开发人员每页加载只注册一个 serviceWorker,并且他们不会更改他们部署到服务器的 serviceWorker 文件的名称。

Workbox 类通过将所有 serviceWorker 注册分为两类来包含 serviceWorker 生命周期的这个更简单的视图:实例自己的注册 serviceWorker 和外部 serviceWorker:

  • 注册 serviceWorker:由于 Workbox 实例调用 register() 而已开始安装的 serviceWorker,或者如果调用 register() 未在注册时触发 updatefound 事件,则已启用安装 serviceWorker。
  • 外部 serviceWorker:一个 serviceWorker,开始独立于 Workbox 实例调用 register() 安装。 当用户在另一个标签页中打开新版本的网站时,通常会发生这种情况。

我们的想法是,来自 serviceWorker 的所有生命周期事件都是你的代码应该期待的事件,而来自外部 serviceWorker 的所有生命周期事件都应该被视为具有潜在危险,并且应该相应地警告用户。

考虑到这两类 serviceWorker,下面是所有重要serviceWorker 生命周期时刻的细分,以及开发人员如何处理它们的建议:

第一次安装 serviceWorker

你可能希望在 serviceWorker 第一次安装时不同于处理所有未来更新的方式。

在 Workbox 中,你可以通过检查以下任何事件的 isUpdate 属性来区分版本首次安装和未来更新。 对于第一次安装,isUpdate 将为 false。

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

wb.addEventListener('installed', (event) => {
  if (!event.isUpdate) {
    // 在这里编写第一次安装需要的代码
  }
});

wb.register();
时刻 事件 建议操作
新的 serviceWorker 已安装(第一次) installed serviceWorker 第一次安装时,通常会预先缓存网站离线工作所需的所有资源。 你可以考虑通知用户他们的站点现在可以离线运行。

此外,由于 serviceWorker 第一次安装它时不会截获该页面加载的获取事件,你也可以考虑缓存已加载的资源(尽管如果这些资源已经被预先缓存,则不需要这样做)。 向上面的缓存示例发送 serviceWorker 的URL列表显示了如何执行此操作。
serviceWorker 已经控制页面 controlling 安装新 serviceWorker 程序并开始控制页面后,所有后续获取事件都将通过该 serviceWorker 程序。 如果你的 serviceWorker 添加了任何特殊逻辑来处理特定的 fetch 事件,那么当你知道逻辑将运行时就是这一点。

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

workbox-window 的角度来看,这意味着仅在 serviceWorker 调用 clients.claim() 的情况下才调度 controlling 事件。 如果在注册之前已经控制了页面,则不会调度此事件。
serviceWorker 已经完成激活 activated 如上所述,serviceWorker 第一次完成激活它可能(或可能不)已经开始控制页面。

因此,你不应该将 activate 事件视为了解 serviceWorker 何时控制页面的方式。 但是,如果你在活动事件中(在 serviceWorker )运行逻辑,并且你需要知道该逻辑何时完成,则激活的事件将让你知道。

发现 serviceWorker 的更新版本时

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

在这种情况下,你的反应通常与第一次安装不同,因为你必须管理用户何时以及如何获得此更新。

时刻 事件 建议操作
已安装新 serviceWorker(更新前一个) installed 如果这不是第一个 serviceWorker 安装(event.isUpdate === true),则表示已找到并安装了较新版本的 serviceWorker(即,与当前控制页面的版本不同)。

这通常意味着已将更新版本的站点部署到你的服务器,并且新资源可能刚刚完成预先缓存。

注意:某些开发人员使用已安装的事件来通知用户其新版本的站点可用。 但是,根据我是否在安装 serviceWorker 程序中调用 skipWaiting(),安装的 serviceWorker 可能会立即生效,也可能不会立即生效。 如果你确实调用 skipWaiting(),那么最好在新 serviceWorker 激活后通知用户更新,如果你没有调用 skipWaiting,最好通知他们等待事件中的挂起更新(见下文了解更多信息) 细节)。
serviceWorker 已安装,但它仍处于等待阶段 waiting 如果 serviceWorker 的更新版本在安装时未调用skipWaiting(),则在当前活动 serviceWorker 控制的所有页面都已卸载之前,它不会激活。 你可能希望通知用户更新可用,并将在下次访问时应用。

警告! 开发人员通常会提示用户重新加载以获取更新,但在许多情况下刷新页面不会激活已安装的工作程序。 如果用户刷新页面并且serviceWorker 仍在等待,则等待事件将再次触发,并且 event.wasWaitingBeforeRegister 属性将为 true。 请注意,我们计划在将来的版本中改进此体验。 关注问题#1848以获取更新。

另一种选择是提示用户并询问他们是否想要获得更新或继续等待。 如果选择获取更新,则可以使用 postMessage() 告诉 serviceWorker 运行 skipWaiting()。 有关示例,请参阅高级配方为用户提供页面重新加载。
serviceWorker 已开始控制页面 controlling 当更新的 serviceWorker 开始控制页面时,这意味着当前控制的 serviceWorker 的版本与加载页面时控制的版本不同。 在某些情况下可能没问题,但也可能意味着当前页面引用的某些资源不再位于缓存中(也可能不在服务器上)。 你可能需要考虑通知用户页面的某些部分可能无法正常工作。

注意:如果不在serviceWorker 中调用 skipWaiting(),则不会触发控制事件。
serviceWorker 已完成激活 activated 当更新的 serviceWorker 完成激活时,这意味着你在 serviceWorker 的激活中运行的任何逻辑都已完成。 如果有什么需要延迟,直到逻辑完成,这是运行它的时间。

找到意外版本的 serviceWorker

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

考虑这样一种情况,即您的网站的标签 A 正在运行 v1,标签 B 正在运行 v2。 加载选项卡 B 时,它将由 v1 附带的 serviceWorker 版本控制,但服务器返回的页面(如果使用网络优先缓存策略用于导航请求)将包含所有 v2 资源。

这对于选项卡 B 来说通常不是问题,因为当你编写 v2 代码时,你知道你的 v1 代码是如何工作的。但是,它可能是标签A的问题,因为你的 v1 代码无法预测你的 v2 代码可能会引入哪些更改。

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

时刻 事件 建议操作
已安装外部 serviceWorker externalinstalled 如果已安装外部 serviceWorker,则可能意味着用户在不同的选项卡中运行你网站的较新版本。

如何响应可能取决于已安装的服务是进入等待还是活动阶段。
通过等待激活来安装外部 serviceWorker externalwaiting 如果外部 serviceWorker 正在等待激活,则可能意味着用户试图在另一个选项卡中获取你网站的新版本,但是由于此选项卡仍处于打开状态,因此他们已被阻止。

如果发生这种情况,你可以考虑向用户显示通知,要求他们关闭此标签。 在极端情况下,你甚至可以考虑调用 window.reload(),如果这样做不会导致用户丢失任何已保存的状态。
serviceWorker 外部 serviceWorker 已激活 externalactivated 如果外部 serviceWorker 程序已激活,则当前页面很可能无法继续正常运行。 你可能需要考虑向用户显示他们正在运行旧版本页面的通知,并且可能会出现问题。

避免常见错误

Workbox 提供的最有用的功能之一是它的开发人员日志记录。 对于 worbox-window 也是这样。

我们知道与 serviceWorker 一起开发往往会让人感到困惑,当事情发生与你期望的相反时,很难知道原因。

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

但是当使用 Workbox 类注册 serviceWorker 时,你将被告知开发人员控制台中的所有生命周期状态更改,这应该有助于调试为什么事情不像你期望的那样。

此外,开发人员在首次使用 serviceWorker 时常犯的错误是在错误的范围内注册 serviceWorker。

为了防止这种情况发生,Workbox类将警告您注册服务工作者的页面是否不在该服务工作者的范围内。 如果您的服务工作者处于活动状态但尚未控制该页面,它还会警告你:

window 到 serviceWorker 的沟通

大多数高级 serviceWorker 使用涉及 serviceWorker 和 window 之间的消息传递丢失。 Workbox 类通过提供 messageSW() 方法来帮助解决这个问题,该方法将postMessage() 实例的注册 serviceWorker 并等待响应。

虽然你可以以任何格式向 serviceWorker 发送数据,但所有 Workbox 包共享的格式是具有三个属性的对象(后两个是可选的):

属性 必须 类型 描述
type string 标识此消息的唯一字符串。

按照惯例,类型都是大写的,下划线分隔单词。 如果类型表示要采取的动作,则它应该是现在时的命令(例如 CACHE_URLS ),如果类型表示报告的信息,则它应该是过去时(例如 URLS_CACHED )。
meta string 在 Workbox 中,这始终是发送消息的 Workbox 包的名称。 自己发送邮件时,可以省略此属性或将其设置为你喜欢的任何内容。
payload * 正在发送的数据。 通常这是一个对象,但它不一定是。

通过 messageSW() 方法发送的消息使用 MessageChannel,因此接收方可以响应它们。 要响应消息,你可以在消息事件侦听器中调用 event.ports[0].postMessage(response)。 messageSW() 方法返回一个 promise,该 promise 将解析为你返回的任何响应。

这是一个从 window 到 serviceWorker 发送消息并获得响应的示例。 第一个代码块是 serviceWorker 中的消息侦听器,第二个块使用 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 中的代码(运行在 window 环境):

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

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

管理版本不兼容性

上面的示例显示了如何从 window 中实现检查 serviceWorker 版本。 使用此示例是因为当你在 window 和 serviceWorker 之间来回发送消息时,请务必注意你的 serviceWorker 可能没有运行与你的页面代码运行相同的站点版本,以及 处理此问题的解决方案会有所不同,具体取决于你是以网络优先服务还是缓存优先服务。

网络优先

首先为你的网页提供服务时,你的用户将始终从你的服务器获取最新版本的 HTML。 但是,当用户第一次重新访问你的站点时(在部署更新之后),他们获得的 HTML 将是最新版本,但在其浏览器中运行的 serviceWorker 将是先前安装的版本(可能是许多旧版本)。

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

因此,在进行任何关键工作之前,始终对 serviceWorker 进行版本控制并检查兼容版本是个好主意。

例如,在上面的代码中,如果该 messageSW() 调用返回的 serviceWorker 版本早于预期版本,则最好等到找到更新(这应该在调用 register() 时发生)。 此时,你可以通知用户或更新,也可以手动跳过等待阶段以立即激活新的 serviceWorker。

缓存优先

与在网络服务页面时相比,首先,当你首先提供页面缓存时,你知道你的页面最初将始终与 serviceWorker 的版本相同(因为这是服务它的原因)。 因此,立即使用messageSW() 是安全的。

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

管理这种可能性的一种策略是使用版本控制方案,允许你区分中断更新和非中断更新,并且在更新中断的情况下,你知道向 serviceWorker 发送消息是不安全的。 相反,你需要警告用户他们正在运行旧版本的页面,并建议他们重新加载以获取更新。


博客名称:王乐平博客

CSDN博客地址:blog.csdn.net/lecepin

知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。