前言
ServiceWorker 运行在worker context,无法直接访问DOM,那么它如何与其控制的页面进行通信呢?本文详细介绍ServiceWorker与其控制的页面之间的通信机制。
单向通信
(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不能回复消息响应给页面。
双向通信
(1)页面建立MessageChannel,使用MessageChannel.port1监听来自ServiceWorker的消息。使用ServiceWorker.postMessage发送消息给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事件,即可获取到页面发过来的消息。同时,它可使用页面传递过来的MessageChannel.port2(即event.ports[0])的 postMessage 方法回复消息给页面。
sw.js
self.addEventListener('message', function(event) { var data = event.data; if (data.command == "twoWayCommunication") { event.ports[0].postMessage({ "message": "Hi, Page" }); } });
广播通信
(1)页面使用ServiceWorker.postMessage发送消息给ServiceWorker,要求它向所有Client广播消息。同时,注册onmessage事件以监听ServiceWorker的广播消息。
script.js
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" });
} }
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" }); }) }) } });
注:上述例子来自参考文档
MessageChannel原理
我们重点讨论一下双向通信中提到的MessageChannel,理解它的原理和可能存在的问题。
(1)页面创建MessageChannel
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.postMessage向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
双向通信的问题
(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进行多次双向通信。这个点请务必留意,否则可能会有意想不到的错误。
参考文档
ServiceWorker Communication via MessageChannel
Message ports aren't properly transfered in messages to service workers
Service Worker postMessage() Sample