天天看點

PWA系列 - Service Workers 消息通信前言 單向通信 雙向通信 廣播通信 MessageChannel原理 雙向通信的問題 參考文檔

前言

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進行多次雙向通信。這個點請務必留意,否則可能會有意想不到的錯誤。

https://www.atatech.org/articles/77919#6 參考文檔

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

繼續閱讀