PWA系列 - Service Workers 消息通信

前言

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"
        });
    }
}
 
(2)ServiceWorker监听onmessage事件,获取到页面发过来的广播请求。ServiceWorker遍历所有的 Client,并使用 Client.postMessage发送消息给每一个 Client,从而实现消息广播。
sw.js
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

Communication between SW and Pages

MDN - MessagePort

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值