与 Service Worker 的双向通信

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

在某些情况下,Web 应用可能需要在网页与服务工作线程之间建立双向通信渠道。

例如,在播客 PWA 中,可以构建一项功能,让用户下载剧集以供离线收听,并允许服务工作线程定期将进度告知网页,以便主线程更新界面。

在本指南中,我们将通过探索不同的 API、Workbox 库以及一些高级用例,探讨在 Windowservice worker 上下文之间实现双向通信的不同方式。

显示服务工作线程和网页交换消息的图表。

使用 Workbox

workbox-window 是一组旨在在窗口上下文中运行的 Workbox 库模块。Workbox 类提供了一个 messageSW() 方法,用于向实例的已注册服务工作线程发送消息并等待响应。

以下网页代码会创建一个新的 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);

服务工作线程在另一端实现了一个消息监听器,并响应已注册的服务工作线程:

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

在底层,该库使用我们将在下一部分中介绍的浏览器 API:Message Channel,但它抽象了许多实现细节,使其更易于使用,同时利用了该 API 具有的广泛的浏览器支持

显示网页与 service worker 之间使用 Workbox Window 进行双向通信的图表。

使用浏览器 API

如果 Workbox 库无法满足您的需求,您可以使用多个较低级别的 API 来实现网页与服务工作线程之间的“双向”通信。它们有一些相似之处和不同之处:

相似之处:

  • 在所有情况下,通信都从一端通过 postMessage() 接口开始,并通过实现 message 处理程序在另一端接收。
  • 实际上,所有可用的 API 都允许我们实现相同的使用情形,但其中一些 API 可能会在某些情况下简化开发。

不同之处:

  • 它们识别通信另一端的方式各不相同:有些使用对另一上下文的显式引用,而另一些则可以通过在每一端实例化的代理对象进行隐式通信。
  • 浏览器支持情况因设备而异。
图表:显示了网页与服务工作线程之间的双向通信,以及可用的浏览器 API。

Broadcast Channel API

Browser Support

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Source

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 等其他浏览器尚不支持该 API

Client API

Browser Support

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Source

借助 Client API,您可以获取对所有 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
  }
});

为了与任何客户端进行通信,服务工作线程会通过执行 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 是一种不错的选择,可用于以相对简单的方式轻松地与服务工作线程中的所有活动标签页进行通信。所有主要浏览器均支持此 API,但并非所有方法都可用,因此请务必先检查浏览器支持情况,然后再在您的网站中实现此 API。

消息渠道

Browser Support

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Source

消息通道需要定义一个端口并将其从一个上下文传递到另一个上下文,以建立双向通信通道。

为了初始化渠道,网页会实例化一个 MessageChannel 对象,并使用该对象向已注册的服务工作线程发送端口。该网页还实现了 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。

后台同步

Browser Support

  • Chrome: 49.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

聊天应用可能希望确保消息永远不会因连接不良而丢失。借助后台同步 API,您可以延迟执行操作,以便在用户拥有稳定的连接时重试。这有助于确保用户想要发送的内容能够实际发送。

该网页注册的是 sync,而不是 postMessage() 接口:

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

然后,服务工作线程会监听 sync 事件以处理消息:

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

函数 doSomeStuff() 应返回一个 promise,指明其尝试执行的操作是成功还是失败。如果该 promise 成功兑现,则同步完成。如果失败,系统会安排另一次同步以进行重试。重试同步也会等待连接,并采用指数退避算法。

执行完操作后,服务工作线程可以使用之前探索过的任何通信 API 与网页进行通信,以更新界面。

Google 搜索使用后台同步来保留因连接不良而失败的查询,并在用户在线时稍后重试这些查询。执行操作后,它们会通过 Web 推送通知将结果告知用户:

图表:显示网页如何将端口传递给服务工作线程,以建立双向通信。

后台提取

Browser Support

  • Chrome: 74.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

对于发送消息或缓存网址列表等相对较短的工作,目前探索的选项是不错的选择。如果任务耗时过长,浏览器会终止服务工作线程,否则会给用户的隐私和电池带来风险。

借助后台提取 API,您可以将耗时较长的任务(例如下载电影、播客或游戏关卡)分流给服务工作线程。

如需从网页向服务工作线程发送消息,请使用 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 以提前缓存资源(例如在预提取场景中)。
  • 广播更新:从服务工作线程调用网页,以告知重要更新(例如,有新版本的 Web 应用可用)。