与 Service Worker 的双向通信

在某些情况下,Web 应用可能需要在页面和 Service Worker 之间建立双向通信通道。

例如:在播客 PWA 中,可以构建一项功能,让用户下载剧集以供离线收听,并允许 Service Worker 定期向页面通报进度,以便 主线程 可以更新界面。

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

Diagram showing a service worker and the page exchanging messages.

使用 Workbox

workbox-windowWorkbox 库的一组模块,旨在在 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 的广泛浏览器支持

Diagram showing two-way communication between page and service worker, using Workbox Window.

使用浏览器 API

如果 Workbox 库不足以满足您的需求,则可以使用多个较低级别的 API 来实现页面和 Service Worker 之间的“双向”通信。它们有一些相似之处和不同之处

相似之处

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

不同之处

  • 它们具有不同的方式来识别通信的另一方:其中一些使用对另一个上下文的显式引用,而另一些可以通过在每一端实例化的代理对象隐式地进行通信。
  • 它们之间的浏览器支持各不相同。
Diagram showing two-way communication between page and service worker, and the available browser APIs.

Broadcast Channel API

浏览器支持

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

来源

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 或任何特定客户端的引用。

Diagram showing two-way communication between page and service worker, using a Broadcast Channel object.

缺点是,在撰写本文时,该 API 受 Chrome、Firefox 和 Edge 支持,但其他浏览器(如 Safari)尚不支持

Client API

浏览器支持

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

来源

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'});
  }
});
Diagram showing a service worker communicating with an array of clients.

Client API 是一个不错的选择,可以轻松地从 Service Worker 以相对直接的方式与所有活动的标签页进行通信。该 API 受 所有主流浏览器支持,但并非所有方法都可用,因此在您的网站中实现之前,请务必检查浏览器支持。

消息通道

浏览器支持

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

来源

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

要初始化通道,页面实例化一个 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
};
Diagram showing a page passing a port to a service worker, to establish two-way communication.

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 来处理特定场景:缺少连接和长时间下载。

后台同步

浏览器支持

  • Chrome: 49.
  • Edge: 79.
  • Firefox:不支持。
  • Safari:不支持。

来源

聊天应用可能希望确保消息永远不会因连接不良而丢失。后台同步 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 通知将结果传达给用户

Diagram showing a page passing a port to a service worker, to establish two-way communication.

后台提取

浏览器支持

  • Chrome: 74.
  • Edge: 79.
  • Firefox:不支持。
  • Safari:不支持。

来源

对于相对较短的工作,例如发送消息或要缓存的 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}%`);
});
Diagram showing a page passing a port to a service worker, to establish two-way communication.
UI 已更新以指示下载进度(左)。由于 Service Worker,当所有标签页都已关闭时,操作可以继续运行(右)。

后续步骤

在本指南中,我们探讨了页面和 Service Worker 之间通信的最一般情况(双向通信)。

很多时候,可能只需要一个上下文与另一个上下文通信,而无需接收响应。请查看以下指南,了解如何从页面和 Service Worker 实现单向技术,以及用例和生产示例

  • 命令式缓存指南:从页面调用 Service Worker 以预先缓存资源(例如,在预取场景中)。
  • 广播更新:从 Service Worker 调用页面以通知重要更新(例如,Web 应用的新版本可用)。