天天看點

如何與 Service Worker 通信

Service Worker 很棒。它們使 Web 開發人員可以實作以前原生應用專有的類似功能。這類功能是例如推送通知或背景同步的離線功能。

它們是漸進式 Web 應用的核心。但是在設定它們之後,似乎很難完成涉及與 Web 應用互動的更複雜的事情。

在本文中,我将展示可用的選擇并最後進行比較。

Service Worker 與 Web Worker

如果你檢視 Service Workers 的 API,将會看到 Web Worker 和 Service 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個步驟。

  1. 在兩側設定事件偵聽器以接收 'message' 事件
  2. 通過發送 port 并将其存儲在 Service Worker 中,建立與 Service Worker 的連接配接。
  3. 使用存儲的 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