13
作者:Felix Gerschau

翻译:疯狂的技术宅

原文:https://felixgerschau.com/how...

未经允许严禁转载

Service Worker 很棒。它们使 Web 开发人员可以实现以前原生应用专有的类似功能。这类功能是例如推送通知后台同步的离线功能。

它们是渐进式 Web 应用的核心。但是在设置它们之后,似乎很难完成涉及与 Web 应用交互的更复杂的事情。

在本文中,我将展示可用的选择并最后进行比较。

Service Worker 与 Web Worker

如果你查看 Service Workers 的 API,将会看到 Web Worker 和 Service Worker 有非常相似的接口。尽管有相似之处,但它们的意图和功能却大不相同:

image.png

  • Service Worker 可以拦截请求并将其替换为自己缓存中的项目,因此它们的行为就像是代理服务器。他们为 Web 应用提供了“离线功能”。

它们可以在多个标签中使用,甚至在所有标签关闭后仍然可以使用。

  • 另一方面,Web worker 有不同的用途。它们为单线程 JavaScript 语言提供了多线程功能,并用于执行计算繁重的任务,这些任务不应干扰 UI 的响应能力。

它们仅限于一个标签

两者的共同点是它们无权访问 DOM,无法使用 postMessage API 进行通信。你可以将它们看作是具有扩展功能的 Web Worker。

如果你想了解有关它们更多信息,请查看这个对话,尽管有些陈旧,但可以个很好的概述这个话题。到 2020 年,Service Workers 的浏览器支持有了很大的改进。

如何与 Service Worker 通信

选择要向其发送消息的 Service Worker

对于任何来源,都可以有多个 Service Worker。以下内容返回当前控制页面的活动 Service Worker:

navigator.serviceWorker.controller

如果要访问其他 Service Worker,则可以通过 registration 接口访问,该借口使你可以访问以下位置的 Service Worker 状态:

  • ServiceWorkerRegistration.installing
  • ServiceWorkerRegistration.waiting - 已安装此 Service Worker,但尚未激活
  • ServiceWorkerRegistration.active -此Service Worker正在控制当前页面

你可以通过几种不同的方式访问 registration 接口。其中有一个 navigator.serviceWorker.ready。它将返回一个可以通过注册解决的 promise:

navigator.serviceWorker.ready.then((registration) => {
  // At this point, a Service Worker is controlling the current page
});

如果你想了解有关 Service Worker 生命周期的更多信息,请查看这篇文章:(https://bitsofco.de/the-servi...)。

发送信息

正如我已经提到的,Service Worker 通过 postMessage API 进行通信。这不仅允许他们与JavaScript主线程交换数据,而且还可以将消息从一个Service Worker发送到另一个Service Worker。

// app.js - Somewhere in your web app
navigator.serviceWorker.controller.postMessage({
  type: 'MESSAGE_IDENTIFIER',
});
// service-worker.js
// On the Service Worker side we have to listen to the message event
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MESSAGE_IDENTIFIER') {
    // do something
  }
});

这种单向通信的用例是在等待服务的 Service Worker 中调用 skipWaiting,然后将其传递为活动状态并控制页面。这已在 Create-React-App 附带的 Service Worker 中实现。我用此技术在渐进式 Web 应用中显示更新通知,我在这篇文章(https://felixgerschau.com/cre...)中进行了解释。

但是如果你想将消息发送回 Window上下文甚至其他 Service Worker,该怎么办?

Service Worker - Client 通信

有好几种方法可以将消息发送到 Service Worker 的客户端:

  • Broadcast Channel API 允许浏览上下文之间进行通信。此 API 允许上下文之间进行通信,而无需引用。Chrome、Firefox 和 Opera 目前支持该功能。能够建立多对多广播通信。
  • MessageChannel API 它可用于在 Window 和 Service Worker 上下文之间建立一对一通信。
  • Service Worker 的 Clients 接口。它可用于向 Service Worker 的一个或多个客户端进行广播。

我将为你提供每个方法的简短示例,然后将它们进行比较,以查看哪种方法最适合你的用例。

我没有包含 FetchEvent.respondWith(),因为这仅适用于获取事件,而且目前不受 Safari 浏览器支持。

使用 MessageChannel API

顾名思义,MessageChannel API 设置了一个可以发送消息的通道。

该实现可以归结为3个步骤。

  1. 在两侧设置事件侦听器以接收 'message' 事件
  2. 通过发送 port 并将其存储在 Service Worker 中,建立与 Service Worker 的连接。
  3. 使用存储的 port 回复客户端

也可以添加第四步,如果你想通过在 Service Worker 中调用 port.close() 来关闭连接的话。

在实践中看起来像这样:

// app.js - somewhere in our main app
const messageChannel = new MessageChannel();

// First we initialize the channel by sending
// the port to the Service Worker (this also
// transfers the ownership of the port)
navigator.serviceWorker.controller.postMessage({
  type: 'INIT_PORT',
}, [messageChannel.port2]);

// Listen to the response
messageChannel.port1.onmessage = (event) => {
  // Print the result
  console.log(event.data.payload);
};

// Then we send our first message
navigator.serviceWorker.controller.postMessage({
  type: 'INCREASE_COUNT',
});
// service-worker.js
let getVersionPort;
let count = 0;
self.addEventListener("message", event => {
  if (event.data && event.data.type === 'INIT_PORT') {
    getVersionPort = event.ports[0];
  }

  if (event.data && event.data.type === 'INCREASE_COUNT') {
    getVersionPort.postMessage({ payload: ++count });
  }
}

使用 Broadcast API

Broadcast API 与 MessageChannel 非常相似,但是它消除了将端口传递给 Service Worker 的需求。

在这个例子中,我们看到只需要在两侧建立一个有相同名称 count-channel 的通道。

我们可以将相同的代码添加到其他 WebWorker 或 Service Worker,后者也将接收所有这些消息。

在这里,我们从上方看到了相同的例子,但用了 Broadcast API:

// app.js
// Set up channel
const broadcast = new BroadcastChannel('count-channel');

// Listen to the response
broadcast.onmessage = (event) => {
  console.log(event.data.payload);
};

// Send first request
broadcast.postMessage({
  type: 'INCREASE_COUNT',
});
// service-worker.js
// Set up channel with same name as in app.js
const broadcast = new BroadcastChannel('count-channel');
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'INCREASE_COUNT') {
    broadcast.postMessage({ payload: ++count });
  }
};

使用 Client API

Client API 也不需要传递对通道的引用。

在客户端,我们侦听 Service Worker 的响应,在 Service Worker 中,用 self.clients.matchAll 函数提供给我们的过滤器选项,选择要发送响应的客户端。

// app.js
// Listen to the response
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'REPLY_COUNT_CLIENTS') {
    setCount(event.data.count);
  }
};

// Send first request
navigator.serviceWorker.controller.postMessage({
  type: 'INCREASE_COUNT_CLIENTS',
});
// service-worker.js
// Listen to the request
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'INCREASE_COUNT') {
    // Select who we want to respond to
    self.clients.matchAll({
      includeUncontrolled: true,
      type: 'window',
    }).then((clients) => {
      if (clients && clients.length) {
        // Send a response - the clients
        // array is ordered by last focused
        clients[0].postMessage({
          type: 'REPLY_COUNT',
          count: ++count,
        });
      }
    });
  }
});

总结

postMessage API提供了一个简单灵活的接口,使我们可以将消息发送给 Service Worker。

Broadcast Channel API 是最容易使用的选项,但不幸的是,它的浏览器支持并不是很好。

在剩下的两个中,我更喜欢 Client API,因为这不需要将引用传递给 Service Worker。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:



疯狂的技术宅
44.4k 声望39.2k 粉丝