Service Worker 很棒。它们使 Web 开发人员可以实现以前原生应用专有的类似功能。这类功能是例如推送通知或后台同步的离线功能。
它们是渐进式 Web 应用的核心。但是在设置它们之后,似乎很难完成涉及与 Web 应用交互的更复杂的事情。
在本文中,我将展示可用的选择并最后进行比较。
Service Worker 与 Web Worker
如果你查看 Service Workers 的 API,将会看到 Web Worker 和 Service Worker 有非常相似的接口。尽管有相似之处,但它们的意图和功能却大不相同:
Service Worker vs Web Worker
- Service Worker 可以拦截请求并将其替换为自己缓存中的项目,因此它们的行为就像是代理服务器。他们为 Web 应用提供了“离线功能”。
它们可以在多个标签中使用,甚至在所有标签关闭后仍然可以使用。
- 另一方面,Web worker 有不同的用途。它们为单线程 JavaScript 语言提供了多线程功能,并用于执行计算繁重的任务,这些任务不应干扰 UI 的响应能力。
它们仅限于**一个标签 **。
两者的共同点是它们无权访问 DOM,无法使用 postMessage API 进行通信。你可以将它们看作是具有扩展功能的 Web Worker。
如果你想了解有关它们更多信息,请查看这个对话,尽管有些陈旧,但可以个很好的概述这个话题。到 2020 年,[Service Workers 的浏览器支持](https://caniuse.com/#search=service worker)有了很大的改进。
如何与 Service Worker 通信
选择要向其发送消息的 Service Worker
对于任何来源,都可以有多个 Service Worker。以下内容返回当前控制页面的活动 Service Worker:
1navigator.serviceWorker.controller
如果要访问其他 Service Worker,则可以通过 registration 接口访问,该借口使你可以访问以下位置的 Service Worker 状态:
-
ServiceWorkerRegistration.installing
-
ServiceWorkerRegistration.waiting - 已安装此 Service Worker,但尚未激活
-
ServiceWorkerRegistration.active -此Service Worker正在控制当前页面
你可以通过几种不同的方式访问 registration 接口。其中有一个 navigator.serviceWorker.ready。它将返回一个可以通过注册解决的 promise:
1navigator.serviceWorker.ready.then((registration) => {
2 // At this point, a Service Worker is controlling the current page
3});
如果你想了解有关 Service Worker 生命周期的更多信息,请查看这篇文章:(https://bitsofco.de/the-service-worker-lifecycle/)。
发送信息
正如我已经提到的,Service Worker 通过 postMessage API 进行通信。这不仅允许他们与JavaScript主线程交换数据,而且还可以将消息从一个Service Worker发送到另一个Service Worker。
1// app.js - Somewhere in your web app
2navigator.serviceWorker.controller.postMessage({
3 type: 'MESSAGE_IDENTIFIER',
4});
5// service-worker.js
6// On the Service Worker side we have to listen to the message event
7self.addEventListener('message', (event) => {
8 if (event.data && event.data.type === 'MESSAGE_IDENTIFIER') {
9 // do something
10 }
11});
这种单向通信的用例是在等待服务的 Service Worker 中调用 skipWaiting,然后将其传递为活动状态并控制页面。这已在 Create-React-App 附带的 Service Worker 中实现。我用此技术在渐进式 Web 应用中显示更新通知,我在这篇文章(https://felixgerschau.com/create-a-pwa-update-notification-with-create-react-app)中进行了解释。
但是如果你想将消息发送回 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个步骤。
-
在两侧设置事件侦听器以接收 'message' 事件
-
通过发送 port 并将其存储在 Service Worker 中,建立与 Service Worker 的连接。
-
使用存储的 port 回复客户端
也可以添加第四步,如果你想通过在 Service Worker 中调用 port.close() 来关闭连接的话。
在实践中看起来像这样:
1// app.js - somewhere in our main app
2const messageChannel = new MessageChannel();
3
4// First we initialize the channel by sending
5// the port to the Service Worker (this also
6// transfers the ownership of the port)
7navigator.serviceWorker.controller.postMessage({
8 type: 'INIT_PORT',
9}, [messageChannel.port2]);
10
11// Listen to the response
12messageChannel.port1.onmessage = (event) => {
13 // Print the result
14 console.log(event.data.payload);
15};
16
17// Then we send our first message
18navigator.serviceWorker.controller.postMessage({
19 type: 'INCREASE_COUNT',
20});
21// service-worker.js
22let getVersionPort;
23let count = 0;
24self.addEventListener("message", event => {
25 if (event.data && event.data.type === 'INIT_PORT') {
26 getVersionPort = event.ports[0];
27 }
28
29 if (event.data && event.data.type === 'INCREASE_COUNT') {
30 getVersionPort.postMessage({ payload: ++count });
31 }
32}
使用 Broadcast API
Broadcast API 与 MessageChannel 非常相似,但是它消除了将端口传递给 Service Worker 的需求。
在这个例子中,我们看到只需要在两侧建立一个有相同名称 count-channel 的通道。
我们可以将相同的代码添加到其他 WebWorker 或 Service Worker,后者也将接收所有这些消息。
在这里,我们从上方看到了相同的例子,但用了 Broadcast API:
1// app.js
2// Set up channel
3const broadcast = new BroadcastChannel('count-channel');
4
5// Listen to the response
6broadcast.onmessage = (event) => {
7 console.log(event.data.payload);
8};
9
10// Send first request
11broadcast.postMessage({
12 type: 'INCREASE_COUNT',
13});
14// service-worker.js
15// Set up channel with same name as in app.js
16const broadcast = new BroadcastChannel('count-channel');
17broadcast.onmessage = (event) => {
18 if (event.data && event.data.type === 'INCREASE_COUNT') {
19 broadcast.postMessage({ payload: ++count });
20 }
21};
使用 Client API
Client API 也不需要传递对通道的引用。
在客户端,我们侦听 Service Worker 的响应,在 Service Worker 中,用 self.clients.matchAll 函数提供给我们的过滤器选项,选择要发送响应的客户端。
1// app.js
2// Listen to the response
3navigator.serviceWorker.onmessage = (event) => {
4 if (event.data && event.data.type === 'REPLY_COUNT_CLIENTS') {
5 setCount(event.data.count);
6 }
7};
8
9// Send first request
10navigator.serviceWorker.controller.postMessage({
11 type: 'INCREASE_COUNT_CLIENTS',
12});
13// service-worker.js
14// Listen to the request
15self.addEventListener('message', (event) => {
16 if (event.data && event.data.type === 'INCREASE_COUNT') {
17 // Select who we want to respond to
18 self.clients.matchAll({
19 includeUncontrolled: true,
20 type: 'window',
21 }).then((clients) => {
22 if (clients && clients.length) {
23 // Send a response - the clients
24 // array is ordered by last focused
25 clients[0].postMessage({
26 type: 'REPLY_COUNT',
27 count: ++count,
28 });
29 }
30 });
31 }
32});
总结
postMessage API提供了一个简单灵活的接口,使我们可以将消息发送给 Service Worker。
Broadcast Channel API 是最容易使用的选项,但不幸的是,它的浏览器支持并不是很好。
在剩下的两个中,我更喜欢 Client API,因为这不需要将引用传递给 Service Worker。
原文链接
https://felixgerschau.com/how-to-communicate-with-service-workers