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