在某些情况下,Web 应用可能需要在页面与 Service Worker 之间建立双向通信通道。
例如:在播客 PWA 中,您可以构建一项功能,让用户下载剧集以供离线使用,并允许 Service Worker 定期向页面通知进度,以便主线程可以更新界面。
在本指南中,我们将通过探索不同的 API、Workbox 库以及一些高级用例,探索在 Window 和 Service Worker 上下文之间实现双向通信的不同方式。
使用 Workbox
workbox-window 是一组 Workbox 库模块,旨在在窗口上下文中运行。Workbox
类提供了一个 messageSW() 方法,用于向实例的已注册 Service Worker 发送消息并等待响应。
以下页面代码会创建一个新的 Workbox 实例,并向 Service Worker 发送消息以获取其版本:
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:
const SW_VERSION = '1.0.0';
self.addEventListener('message', (event) => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
在后台,该库使用我们将在下一部分中介绍的浏览器 API:消息通道,但它抽象了许多实现细节,使其更易于使用,同时利用了此 API 具有的广泛浏览器支持。
使用浏览器 API
如果 Workbox 库无法满足您的需求,您可以使用多个较低级别的 API 在页面和服务工作器之间实现“双向”通信。它们有一些相似之处和不同之处:
相似之处:
- 在所有情况下,通信都是从一端通过
postMessage()接口开始,并在另一端通过实现message处理程序来接收。 - 在实践中,所有可用的 API 都允许我们实现相同的用例,但其中一些 API 可能会在某些场景中简化开发。
不同之处:
- 它们有不同的方式来识别通信的另一端:有些会显式引用另一端上下文,而有些则可以通过在每一端实例化的代理对象进行隐式通信。
- 这些工具对浏览器的支持情况各不相同。
Broadcast Channel API
Broadcast Channel API 允许通过 BroadcastChannel 对象在浏览上下文之间进行基本通信。
如需实现此功能,首先,每个上下文都必须实例化一个具有相同 ID 的 BroadcastChannel 对象,并从中发送和接收消息:
const broadcast = new BroadcastChannel('channel-123');
BroadcastChannel 对象会公开一个 postMessage() 接口,用于向任何监听上下文发送消息:
//send message
broadcast.postMessage({ type: 'MSG_ID', });
任何浏览器上下文都可以通过 BroadcastChannel 对象的 onmessage 方法监听消息:
//listen to messages
broadcast.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process message...
}
};
如您所见,这里没有明确引用特定上下文,因此无需先获取对 Service Worker 或任何特定客户端的引用。
缺点是,在撰写本文时,该 API 仅受 Chrome、Firefox 和 Edge 支持,而 Safari 等其他浏览器尚不支持。
Client API
借助 Client API,您可以获取对表示 Service Worker 正在控制的活动标签页的所有 WindowClient 对象的引用。
由于该页面由单个 Service Worker 控制,因此它会通过 serviceWorker 接口直接监听活跃 Service Worker 并向其发送消息:
//send message
navigator.serviceWorker.controller.postMessage({
type: 'MSG_ID',
});
//listen to messages
navigator.serviceWorker.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process response
}
};
同样,Service Worker 通过实现 onmessage 监听器来监听消息:
//listen to messages
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//Process message
}
});
为了与任何客户端进行通信,Service Worker 会通过执行 Clients.matchAll() 和 Clients.get() 等方法获取 WindowClient 对象的数组。然后,它可以 postMessage() 其中任何一个:
//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
if (clients && clients.length) {
//Respond to last focused tab
clients[0].postMessage({type: 'MSG_ID'});
}
});
Client API 是一个不错的选择,可让您以相对简单的方式轻松与 Service Worker 中的所有活跃标签页进行通信。所有主流浏览器都支持该 API,但并非所有方法都可用,因此在网站中实现该 API 之前,请务必检查浏览器支持情况。
消息渠道
Message Channel 需要定义一个端口并将其从一个上下文传递到另一个上下文,以建立双向通信通道。
为了初始化通道,页面会实例化一个 MessageChannel 对象,并使用该对象向已注册的 Service Worker 发送一个端口。该页面还实现了一个 onmessage 监听器,用于接收来自其他上下文的消息:
const messageChannel = new MessageChannel();
//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
messageChannel.port2,
]);
//Listen to messages
messageChannel.port1.onmessage = (event) => {
// Process message
};
Service Worker 收到端口后,会保存对该端口的引用,并使用该端口向另一端发送消息:
let communicationPort;
//Save reference to port
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'PORT_INITIALIZATION') {
communicationPort = event.ports[0];
}
});
//Send messages
communicationPort.postMessage({type: 'MSG_ID'});
所有主流浏览器目前都支持 MessageChannel。
高级 API:后台同步和后台提取
在本指南中,我们探讨了如何实现双向通信技术,适用于相对简单的情况,例如传递描述要执行的操作的字符串消息,或从一个上下文传递到另一个上下文的要缓存的网址列表。在本部分中,我们将探索两个 API,以处理特定场景:缺乏连接和下载时间长。
后台同步
聊天应用可能希望确保消息绝不会因连接不良而丢失。Background Sync API 可让您推迟操作,以便在用户拥有稳定的连接性时重试。这有助于确保用户想要发送的所有内容都实际发送。
该页面注册的是 sync,而不是 postMessage() 接口:
navigator.serviceWorker.ready.then(function (swRegistration) {
return swRegistration.sync.register('myFirstSync');
});
然后,Service Worker 会监听 sync 事件以处理消息:
self.addEventListener('sync', function (event) {
if (event.tag == 'myFirstSync') {
event.waitUntil(doSomeStuff());
}
});
函数 doSomeStuff() 应返回一个 promise,指示其尝试执行的操作是成功还是失败。如果成功,则同步完成。如果失败,系统会安排另一次同步以重试。重试同步也会等待连接,并采用指数退避算法。
执行操作后,Service Worker 可以使用前面介绍的任何通信 API 与页面进行通信,以更新界面。
Google 搜索使用后台同步来保留因连接不良而失败的查询,并在用户在线时重试。执行操作后,它们会通过 Web 推送通知将结果传达给用户:
后台提取
对于相对较短的工作(例如发送消息或要缓存的网址列表),到目前为止探索的选项是不错的选择。如果任务耗时过长,浏览器将终止 Service Worker,否则会给用户的隐私和电池带来风险。
Background Fetch API 允许您将长任务(例如下载电影、播客或游戏关卡)分流给 Service Worker。
如需从页面与 Service Worker 通信,请使用 backgroundFetch.fetch,而不是 postMessage():
navigator.serviceWorker.ready.then(async (swReg) => {
const bgFetch = await swReg.backgroundFetch.fetch(
'my-fetch',
['/ep-5.mp3', 'ep-5-artwork.jpg'],
{
title: 'Episode 5: Interesting things.',
icons: [
{
sizes: '300x300',
src: '/ep-5-icon.png',
type: 'image/png',
},
],
downloadTotal: 60 * 1024 * 1024,
},
);
});
BackgroundFetchRegistration 对象允许网页监听 progress 事件,以跟踪下载进度:
bgFetch.addEventListener('progress', () => {
// If we didn't provide a total, we can't provide a %.
if (!bgFetch.downloadTotal) return;
const percent = Math.round(
(bgFetch.downloaded / bgFetch.downloadTotal) * 100,
);
console.log(`Download progress: ${percent}%`);
});
后续步骤
在本指南中,我们探讨了页面与 Service Worker 之间最常见的通信情况(双向通信)。
很多时候,一个上下文可能只需要与另一个上下文通信,而无需接收响应。请参阅以下指南,了解如何在页面中实现与 Service Worker 之间的单向通信技术,以及用例和生产示例: