PWA系列 - Service Workers 消息通信

简介: 本文详细介绍ServiceWorker与其控制的页面之间的通信机制,分析可能存在的问题和提供一些解决思路。

前言

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

目录
相关文章
|
4月前
|
安全 Android开发 开发者
Service与Activity如何实现通信
Android为Service与Activity之间的通信提供了多种灵活的方式,开发者可以根据应用程序的需求选择合适的通信机制。对于多数简单通信需求,Intent和Binder通常就足够使用。另外,要注意线程安全和数据同步的问题,尤其是在多线程环境下操作UI或Service中的共享资源时。
197 0
|
4月前
|
监控 安全 前端开发
【Azure 应用服务】App Service 运行状况健康检查功能简介 (Health check)
【Azure 应用服务】App Service 运行状况健康检查功能简介 (Health check)
109 0
|
4月前
|
微服务 Windows
【Azure微服务 Service Fabric 】在SF节点中开启Performance Monitor及设置抓取进程的方式
【Azure微服务 Service Fabric 】在SF节点中开启Performance Monitor及设置抓取进程的方式
|
4月前
|
存储 网络安全 API
【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 
【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 
|
5月前
|
缓存 JavaScript 前端开发
Web Workers与Service Workers:后台处理与离线缓存
Web Workers 和 Service Workers 是两种在Web开发中处理后台任务和离线缓存的重要技术。它们在工作原理和用途上有显著区别。
73 1
|
7月前
|
负载均衡 Dubbo Java
Service Mesh 的基本模式
【5月更文挑战第16天】Service Mesh分为两种模式:Sidecar和第二代Service Mesh。
|
缓存 JavaScript 前端开发
在项目中使用Service Worker 与 PWA
在项目中使用Service Worker 与 PWA
94 1
|
存储 Web App开发 缓存
WorkBox 之底层逻辑Service Worker(二)
WorkBox 之底层逻辑Service Worker(二)
181 0
|
存储 缓存 前端开发
WorkBox 之底层逻辑Service Worker(一)
WorkBox 之底层逻辑Service Worker(一)
112 0
|
存储 缓存 前端开发
Service Worker实现离线缓存和推送通知
离线缓存和推送通知在提升网页的离线访问体验方面起着重要的作用。 离线缓存允许网页将所需的资源(如 HTML、CSS、JavaScript 文件、图像等)保存在用户设备的本地存储中。这意味着即使在没有网络连接的情况下,用户仍然可以访问网页的内容和功能。离线缓存不仅提供了更好的用户体验,而且还可以减轻服务器的负担,因为客户端可以直接通过本地缓存的资源进行加载,而无需每次都向服务器发出请求。
672 0