与 Service Worker 的双向通信

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

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

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

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 来实现页面和 Service Worker 之间的“双向”通信。它们有一些相似之处和不同之处:

相似之处:

  • 无论哪种情况,通信都通过 postMessage() 接口在一端开始,并通过实现 message 处理程序在另一端接收。
  • 在实践中,所有可用的 API 都允许我们实现相同的用例,但其中一些 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,您可以获取对代表 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 之前,请务必查看浏览器支持情况。

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

为了初始化通道,页面需要实例化 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:缺少连接和下载时间过长。

聊天应用可能想要确保消息不会因网络连接状况不佳而丢失。借助 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,否则会给用户的隐私和电池带来风险。

借助 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 调用页面以通知重要更新(例如,有新版本的 Web 应用可用)。
阅读余下内容
 

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号