前言
ServiceWorker運作在worker context,無法直接通路DOM,那麼它如何與其控制的頁面進行通信呢?本文詳細介紹ServiceWorker與其控制的頁面之間的通信機制。
https://www.atatech.org/articles/77919#1 單向通信
(1)頁面使用
ServiceWorker.postMessage發送消息給ServiceWorker。
script.js
function oneWayCommunication() {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
"command": "oneWayCommunication",
"message": "Hi, SW"
});
}
}
(2)ServiceWorker監聽onmessage事件,即可擷取到頁面發過來的消息。
sw.js
self.addEventListener('message', function(event) {
var data = event.data;
if (data.command == "oneWayCommunication") {
console.log("Message from the Page : ", data.message);
}
});
注:單向通信模式下,頁面可以向ServiceWorker發送消息,但是ServiceWorker不能回複消息響應給頁面。
https://www.atatech.org/articles/77919#2 雙向通信
(1)頁面建立
MessageChannel,使用
MessageChannel.port1監聽來自ServiceWorker的消息。使用
發送消息給ServiceWorker,并且将
MessageChannel.port2也一起傳遞給ServiceWorker。
scirpt.js
function twoWayCommunication() {
if (navigator.serviceWorker.controller) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
console.log("Response from the SW : ", event.data.message);
}
navigator.serviceWorker.controller.postMessage({
"command": "twoWayCommunication",
"message": "Hi, SW"
}, [messageChannel.port2]);
}
}
(2)ServiceWorker監聽onmessage事件,即可擷取到頁面發過來的消息。同時,它可使用頁面傳遞過來的
(即event.ports[0])的
postMessage方法回複消息給頁面。
self.addEventListener('message', function(event) {
var data = event.data;
if (data.command == "twoWayCommunication") {
event.ports[0].postMessage({
"message": "Hi, Page"
});
}
});
https://www.atatech.org/articles/77919#3 廣播通信
發送消息給ServiceWorker,要求它向所有
Client廣播消息。同時,注冊onmessage事件以監聽ServiceWorker的廣播消息。
function registerBroadcastReceiver() {
navigator.serviceWorker.onmessage = function(event) {
var data = event.data;
if (data.command == "broadcastOnRequest") {
console.log("Broadcasted message from the ServiceWorker : ", data.message);
}
};
}
function requestBroadcast() {
registerBroadcastReceiver();
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
"command": "broadcast"
});
}
}
(2)ServiceWorker監聽onmessage事件,擷取到頁面發過來的廣播請求。ServiceWorker周遊所有的
,并使用
Client.postMessage發送消息給每一個
,進而實作消息廣播。
self.addEventListener('message', function(event) {
var data = event.data;
if (data.command == "broadcast") {
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage({
"command": "broadcastOnRequest",
"message": "This is a broadcast on request from the SW"
});
})
})
}
});
注:上述例子來自參考文檔
https://www.atatech.org/articles/77919#4 原理
我們重點讨論一下雙向通信中提到的
,了解它的原理和可能存在的問題。
(1)頁面建立
var messageChannel = new MessageChannel();這個語句會建立一個MessageChannel,在浏覽器核心會進行哪些處理呢?
我們看看代碼執行的流程:
blink::V8MessageChannel::constructorCallback
--> blink::V8MessageChannel::constructorCustom
--> blink::MessageChannel::create
--> new blink::MessageChannel::MessageChannel
--> blink::MessagePort::create
--> new blink::MessagePort
--> blink::MessagePort::entangle
--> blink::WebMessagePortChannel::setClient(MessagePort)
--> content::WebMessagePortChannelImpl::setClient
浏覽器核心在建立MessageChannel的過程中,同時會建立兩個MessagePort,一個用于監聽來自ServiceWorker的消息,另外一個傳遞給ServiceWorker,ServiceWorker可使用它來回複消息。
(2)頁面使用
向ServiceWorker發送消息
navigator.serviceWorker.controller.postMessage 可以向ServiceWorker發送消息,代碼的執行流程如下:
blink::ServiceWorker::postMessage
--> blink::WebServiceWorker::postMessage
--> blink::WebServiceWorkerImpl::postMessage
--> blink::ServiceWorkerDispatcherHost::OnPostMessageToWorker
--> blink::ServiceWorkerScriptContext::OnPostMessage
--> blink::WebServiceWorkerContextProxy::dispatchMessageEvent
--> blink::ServiceWorkerGlobalScopeProxy::dispatchMessageEvent
--> blink::ServiceWorkerGlobalScope::dispatchEvent
--> blink::ServiceWorkerGlobalScope.onmessage
--> 觸發事件 self.addEventListener("message", function (event)
(3)ServiceWorker使用port2回複消息
ServiceWorker使用 event.ports[0].postMessage 可以向控制頁面回複消息。
blink::MessagePortV8Internal::postMessageMethodCallback
--> blink::V8MessagePort::postMessageMethodCustom
--> blink::MessagePort::postMessage
--> blink::WebMessagePortChannel::postMessage
--> content::WebMessagePortChannelImpl::postMessage
--> content::MessagePortService::PostMessage
--> content::MessagePortService::PostMessageTo
--> content::WebMessagePortChannelImpl::OnMessage
--> blink::WebMessagePortChannelClient::messageAvailable
--> blink::MessagePort::messageAvailable
--> blink::MessagePort::dispatchMessages
--> blink::MessagePort::tryGetMessageFrom
--> blink::WebMessagePortChannel::tryGetMessage
--> blink::MessagePort::entanglePorts
--> blink::MessageEvent::create
--> dispatchEvent
--> 觸發事件 messageChannel.port1.onmessage = function(event)
(4)ServiceWorker的StopWorker會觸發MessagePort::close
ServiceWorker的StopWorker會觸發MessagePort::close, MessageChannel會關閉,而且ServiceWorker再次重新開機之後也無法重建原來的Messagechannel。
代碼流程如下:
blink::ServiceWorkerVersion::StopWorker
--> content::EmbeddedWorkerInstance::Stop()
--> content::EmbeddedWorkerRegistry::StopWorker
--> content::EmbeddedWorkerDispatcher::OnStopWorker
--> blink::WebEmbeddedWorkerImpl::terminateWorkerContext
--> blink::WorkerThread::stop
--> blink::WorkerThread::stopInternal
--> blink::WorkerThread::WorkerThreadShutdownStartTask
--> blink::WorkerThread::WorkerThreadShutdownFinishTask
--> blink::WorkerThreadTask::run
--> blink::WorkerThreadShutdownFinishTask::performTask
--> blink::WorkerGlobalScope::clearScript
--> WTF::OwnPtr<blink::WorkerScriptController>::clear
--> blink::DOMWrapperMap<blink::ScriptWrappableBase>::clear
--> blink::MessageEvent::~MessageEvent
--> WTF::RefCounted<blink::MessagePort>::deref
--> blink::MessagePort::~MessagePort
--> blink::MessagePort::close
https://www.atatech.org/articles/77919#5 雙向通信的問題
(1)雙向通信的問題
從上面可以看到,ServiceWorker與其控制的頁面可以通過使用MessageChannel進行雙向通信。MessageChannel會建立兩個MessagePort,其中port1由頁面使用來發送消息給ServiceWorker或監聽來自ServiceWorker的消息,而port2則會傳遞給ServiceWorker,ServiceWorker使用port2回複消息給頁面。
從上面我們還可以看到,ServiceWorker的StopWorker會引起MessagePort的close,MessagePort 在close之後就不能收發消息了。而且,我們還發現,ServiceWorker在restart時,并不能重建原來的MessageChannel,最新的Chromium版本存在同樣的問題。這就意味着,在ServiceWorker Stop之後,整個雙向通信的通道就完全不能使用了。
按照ServiceWorker規範的說明,浏覽器可以在任意需要的時候關閉和重新開機ServiceWorker,這也等同于ServiceWorker與其控制頁面建立的MessageChannel随時會斷掉,而且無法重建。
(2)解決方案
思路一: 從上面分析可以看到,ServiceWorker的Stop會破壞MessageChannel的通信通道,那麼如果ServiceWorker不會Stop,即在頁面不關閉時保持不退出呢? 理論上MessageChannel也可以繼續保持正常,這是一個解決思路,但這種思路與規範約定的ServiceWorker的生命周期存在沖突。
思路二: ServiceWorker的Stop會破壞MessageChannel,那麼如果我們每次發送消息都建立MessageChannel呢?理論上也是可行的,而且
官方的Demo就是使用了這種方式。它會實作一個sendMessage方法,通過該方法與ServiceWorker進行通信。其中每次調用該方法都會建立新的MessageChannel,詳細代碼實作如下:
function sendMessage(message) {
// This wraps the message posting/response in a promise, which will resolve if the response doesn't
// contain an error, and reject with the error if it does. If you'd prefer, it's possible to call
// controller.postMessage() and set up the onmessage handler independently of a promise, but this is
// a convenient wrapper.
return new Promise(function(resolve, reject) {
var messageChannel = new MessageChannel(); messageChannel.port1.onmessage = function(event) {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
// This sends the message data as well as transferring messageChannel.port2 to the service worker.
// The service worker can then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
// See https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
navigator.serviceWorker.controller.postMessage(message,
[messageChannel.port2]);
});
}
思路二的缺點是, 每次消息通信都需要建立MessageChannel, 這樣它與單向通信相比, 優勢就不明顯了.
綜合來說, ServiceWorker在雙向通信方面, 目前隻能使用MessageChannel完成單次的雙向通信,而不能重用MessageChannel進行多次雙向通信。這個點請務必留意,否則可能會有意想不到的錯誤。