workbox-window
软件包是将在 window
上下文(也就是说,在您的网页内部)中运行的一组模块。它们是对在 Service Worker 中运行的其他 Workbox 软件包的补充。
workbox-window
的主要功能/目标包括:
- 帮助开发者识别 Service Worker 生命周期中最关键的时刻,并更轻松地响应这些时刻,从而简化 Service Worker 注册和更新过程。
- 为了防止开发者犯下最常见的错误。
- 在 Service Worker 中运行的代码与窗口中运行的代码之间可以更轻松地通信。
导入并使用 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 依赖项配合使用的打包器(如 webpack 或 Rollup),则可以使用它们来加载 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.json
中 workbox-window
的 main
和 module
字段引用的 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 的更新版本时
当新的 Service Worker 开始安装但现有版本当前正在控制页面时,以下所有事件的 isUpdate
属性将为 true
。
您在这种情况下的反应通常与首次安装不同,因为您必须管理用户获取此更新的时间和方式。
发现意外的 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 时,您会在开发者控制台中收到所有生命周期状态变化的通知,这有助于调试出现异常情况的原因。
此外,开发者在首次使用 Service Worker 时常犯的一个错误是在错误作用域中注册 Service Worker。
为了防止发生这种情况,如果注册 Service Worker 的页面不在该 Service Worker 的范围内,Workbox
类将会向您发出警告。如果您的 Service Worker 处于活跃状态但尚未控制页面,它也会向您发出警告:
Service Worker 的通信窗口
大多数高级 Service Worker 的使用都涉及在 Service Worker 和窗口之间执行大量的消息传递。Workbox
类也通过提供 messageSW()
方法提供帮助,该方法将对实例注册的 Service Worker 执行 postMessage()
操作并等待响应。
虽然您可以通过任何格式向 Service Worker 发送数据,但所有 Workbox 软件包共享的格式都是一个具有以下三个属性的对象(后两个属性是可选的):
通过 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>