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 依赖项一起使用的 webpack 或 Rollup 等打包工具,则可以使用它们来加载 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-window 在 package.json 中的 main 和 module 字段引用的构建文件被转换为 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博客地址:http://blog.csdn.net/lecepin