在某些情况下,Web 应用可能需要在页面和 Service Worker 之间建立双向通信通道。
例如:在播客 PWA 中,可以构建一项功能,让用户下载剧集以供离线收听,并允许 Service Worker 定期向页面通报进度,以便 主线程 可以更新界面。
在本指南中,我们将通过探索不同的 API、Workbox 库以及一些高级案例,探索在 Window 和 Service Worker 上下文之间实现双向通信的不同方法。

使用 Workbox
workbox-window
是 Workbox 库的一组模块,旨在在 Window 上下文中运行。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 的广泛浏览器支持。

使用浏览器 API
如果 Workbox 库不足以满足您的需求,则可以使用多个较低级别的 API 来实现页面和 Service Worker 之间的“双向”通信。它们有一些相似之处和不同之处
相似之处
- 在所有情况下,通信都从一端通过
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 允许您获取对所有 WindowClient
对象的引用,这些对象表示 Service Worker 正在控制的活动标签页。
由于页面由单个 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 受 所有主流浏览器支持,但并非所有方法都可用,因此在您的网站中实现之前,请务必检查浏览器支持。
消息通道
消息通道 需要定义和传递一个端口从一个上下文到另一个上下文,以建立双向通信通道。
要初始化通道,页面实例化一个 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:后台同步和后台提取
在本指南中,我们探讨了实现双向通信技术的方法,用于相对简单的案例,例如传递描述要执行的操作的字符串消息,或从一个上下文到另一个上下文要缓存的 URL 列表。在本节中,我们将探索两个 API 来处理特定场景:缺少连接和长时间下载。
后台同步
聊天应用可能希望确保消息永远不会因连接不良而丢失。后台同步 API 可让您延迟操作,以便在用户连接稳定时重试。这对于确保用户想要发送的任何内容都实际发送非常有用。
页面不是使用 postMessage()
接口,而是注册 sync
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,指示其尝试执行的操作的成功/失败。如果 Promise 变为 fulfilled 状态,则同步完成。如果 Promise 变为 rejected 状态,则会安排另一个同步进行重试。重试同步也会等待连接,并采用指数退避。
执行操作后,Service Worker 可以通过使用前面探索的任何通信 API 与页面通信以更新 UI。
Google 搜索使用后台同步来持久化因连接不良而失败的查询,并在用户在线时稍后重试。操作执行后,他们通过 Web Push 通知将结果传达给用户

后台提取
对于相对较短的工作,例如发送消息或要缓存的 URL 列表,到目前为止探索的选项都是不错的选择。如果任务耗时过长,浏览器将终止 Service Worker,否则会对用户的隐私和电池造成风险。
后台提取 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 实现单向技术,以及用例和生产示例