与 Service Worker 的双向通信

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

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

例如,在播客 PWA 中,您可以构建一个功能,让用户能够下载分集以供离线观看,并允许 Service Worker 定期告知网页进度,以便主线程更新界面。

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

显示 Service Worker 和页面交换消息的示意图。

使用 Workbox

workbox-windowWorkbox 库中的一组模块,旨在在窗口上下文中运行。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 窗口页面和 Service Worker 之间的双向通信的示意图。

使用浏览器 API

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

相似之处:

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

差异:

  • 它们具有不同的方式来识别通信的另一方:有些方法使用对其他上下文的显式引用,而另一些可以通过每端实例化的代理对象进行隐式通信。
  • 支持的浏览器因浏览器而异。
展示页面和 Service Worker 与可用的浏览器 API 之间的双向通信的示意图。

广播通道 API

浏览器支持

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

显示页面和 Service Worker 使用广播通道对象之间的双向通信的示意图。

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

Client API

浏览器支持

  • 40
  • 17
  • 44
  • 11.1

来源

借助 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'});
  }
});
显示 Service Worker 与一组客户端通信的示意图。

Client API 是一个不错的选择,可让您以相对简单的方式与 Service Worker 的所有活动标签页轻松通信。所有主流浏览器都支持此 API,但并非所有方法都可用,因此在网站上实现该 API 之前,请务必查看浏览器支持情况。

消息渠道

浏览器支持

  • 2
  • 12
  • 41
  • 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
};
显示页面将端口传递给 Service Worker 以建立双向通信的示意图。

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:缺少连接和下载时间过长。

后台同步

浏览器支持

  • 49
  • 79
  • x
  • x

来源

聊天应用可能想要确保消息不会因网络连接状况不佳而丢失。借助 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 搜索会使用后台同步功能保存因连接不良而失败的查询,并在用户联网后重试。执行操作后,应用会通过网页推送通知向用户传达结果:

显示页面将端口传递给 Service Worker 以建立双向通信的示意图。

后台提取

浏览器支持

  • 74
  • 79
  • x
  • x

来源

对于相对较短的工作量(例如发送消息或要缓存的网址列表),目前为止探索过的选项是一个不错的选择。如果任务花费的时间过长,浏览器将终止 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,操作可以在所有标签页都关闭后继续运行(右)。

后续步骤

在本指南中,我们探索了网页和 Service Worker 之间最常见的通信(双向通信)。

很多时候,一个应用可能只需要一个上下文与另一个上下文进行通信,而不收到响应。请查看以下指南,了解如何在网页中实现 Service Worker 之间的单向技术以及用例和生产示例:

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