WebGL Tutorial
and more

相互传送信息

撰写时间:2025-01-02

修订时间:2025-01-02

另一种注册方式

在本页面中,我们使用另一种注册方式。重点考查对象为我们最早遇到的navigator.serviceWorker,其类型为ServiceWorkerContainer

const container = navigator.serviceWorker; pc.log(container); async function doReg() { let reg = await container.getRegistration('./'); if (!reg) { container.register('caching.js', {scope: './'}); } return await container.ready; } let reg = await doReg(); pc.log(reg); pc.log(container.controller === reg.active);

因为要多次使用navigator.serviceWorker,因此先将其值赋于变量container。在doReg方法中,先调用containergetRegistration方法来检查之前是否已经注册。如果没有,调用其register方法来注册。

containerready方法返回一个成功注入了ServiceWorkerRegistrationPromise。因此doReg返回经解析后的ServiceWorkerRegistration的一个实例,最重要的是,该实例中的active worker已被成功设置,而此时,containercontroller属性指向了这个当前正提供服务的Service Worker

因此,尽管一个成功注册的ServiceWorkerRegistration有多个不同状态的Service Workers,且它们会在不同的场合下自动进行轮候转换的复杂操作,但在调用containerready方法后,则可通过其controller属性安全地获取当前正在提供服务的Service Worker

因为controller是全局唯一性的,在访问不同的服务器路径、使用不同的ServiceWorkerRegistration时,都会导致controller自动切换指向不同的Service Worker,因此,我们可以将其视为当前作用域下正在提供服务的Service Worker

第一次访问本页面时,controller可能指向了其他路径下的Service Worker,因此代码:

pc.log(container.controller === reg.active);

的值为false。而第二次刷新本页面,则该值转变为true,就是这个原因。

封装进自定义的类

明白了上述细节,我们可以封装进一个自定义的类ServiceWorkerUtils中。

export class ServiceWorkerUtils { static async GetActiveWorker(scriptURL, scopeURL = './') { const container = navigator.serviceWorker; let reg = await container.getRegistration(scopeURL); if (!reg) { container.register(scriptURL, {scope: scopeURL}); } return container.ready. then(reg => container.controller); } }

短短几行代码,GetActiveWorker进行了检查、注册、等待状态转换并最终返回一个注入了激活的Service WorkerPromise的一系列动作。

之后,像下面这样进行调用:

const { ServiceWorkerUtils } = await import('/js/esm/ServiceWorkerUtils.js'); let activeWorker = await ServiceWorkerUtils.GetActiveWorker('caching.js'); pc.log(activeWorker);

这样,我们在页面端的主线程中就持有了一个当前正在提供服务的activeWorker

activeWorker有一个postMessage方法,下面我们将调用它向Service Worker发送信息。

主线程向Service Worker发送信息

主线程向激活的Service Worker发送信息:

const { ServiceWorkerUtils } = await import('/js/esm/ServiceWorkerUtils.js'); let activeWorker = await ServiceWorkerUtils.GetActiveWorker('caching.js'); let msgWrapper = { type: 'normal', msg: 'Greeting from page.' }; activeWorker.postMessage(msgWrapper);

postMessage的参数可为任意对象而不仅仅是文本,这里,我们将要发送的信息包装为一个对象,type属性表示消息的类型,msg表示要传送的消息文本。

caching.js文件中,其处理来自主线程所发来信息的代码如下:

self.onmessage = (evt) => { let msgWrapper = evt.data; if (msgWrapper.type === 'normal') { console.log('Service worker received the following message:'); console.log(msgWrapper.msg); } ... };

如果msgWrappertype属性值为normal,则直接在终端中输出信息。上面的...为其他代码部分,但与这里的逻辑无关,因此这里暂不显示。

在Chrome的终端中,您将看到其输出:

Service worker received the following message: Greeting from page.

表明Service Worker收到了主线程所发过来的信息。

但问题在于,Service Worker无法直接向主线程反向发送信息。

但这难不倒我们。

实现双向通讯

我们可以通过MessageChannel来实现双向通讯。

const { ServiceWorkerUtils } = await import('/js/esm/ServiceWorkerUtils.js'); let activeWorker = await ServiceWorkerUtils.GetActiveWorker('caching.js'); let messageChannel = new MessageChannel(); pc.log(messageChannel); let msgWrapper = { type: 'MessageChannel', msg: `Hello, Mike. I'm John.` }; activeWorker.postMessage(msgWrapper, {transfer: [messageChannel.port2]}); messageChannel.port1.onmessage = (evt) => { pc.log(evt); pc.log(evt.ports); pc.log(`Message from Service Worker:`); pc.log(evt.data); };

MessageChannelHTML 5规范中所定义的一个类,以通道的方式,在不同环境中实现了一种较为安全的、且可明确授权的双向通讯机制。

在使用上,MessageChannel非常简单。该类实例只有2个属性:port1port2,表示通道的两个通讯端口。编写代码时,只需说明哪一方使用哪个端口就行了。代码:

activeWorker.postMessage(msgWrapper, {transfer: [messageChannel.port2]});

在向activeWorker发送信息时,将port2端口一并传送,这就指定了activeWorker应使用port2端口来发送信息。

port1端口则由网页端的主线程用以接收通道信息:

messageChannel.port1.onmessage = (evt) => { pc.log(evt); pc.log(evt.ports); pc.log(`Message from Service Worker:`); pc.log(evt.data); };

在主线程接收到Service Worker所返回的信息时,从上面的代码可以获知,参数evt的类型为MessageEvent,其data属性即传递的对象。其ports是一个数组。从上面代码运行后输出结果来看,这是一个空数组。

这里这个代表端口数据的数组为何为空数组?因为在主线程中,我们已经可以直接向activeWoker发送信息,因此activeWoker返回信息时无需再将多余的端口一并传送过来。这里打印该属性的目的是,网页端主线程的evtactiveWorker端的evt是同一类型。但由于Service Worker不能访问DOM,因此不方便展示其类型。所以在这里通过本站的PageConsole先予以展示,下面看到activeWorker端的相应代码就会了解原因了。

最后两行代码,将从activeWorker端接收到的信息通过PageConsole在网页中直接输出。

下面是activeWorker端接收数据时的相应代码:

self.onmessage = (evt) => { let msgWrapper = evt.data; ... if (msgWrapper.type === 'MessageChannel') { let personName = /(\w+)\.$/.exec(msgWrapper.msg)[1]; if (evt.ports.length > 0) { evt.ports[0].postMessage(`Hi, ${personName}!`); } } };

如果消息类型为MessageChannel,则先通过正则表达式取出打招呼者的姓名,然后再发送打招呼的信息。

如上面所述,evt.ports是一个数组,由于主线程一并将messageChannel.port2传递了过来,因此这里evt.ports[0]就是所传递过来的端口。activeWorker端则使用它来反向传递信息。

通过这种双向通讯的方式,我们得以将Service Worker所反馈回来的信息放在网页中显示出来,从而克服了Service Worker不能直接访问DOM的不足。

参考资源

Specifications

  1. Service Workers (W3C CR Draft)
  2. MessagePort

Others

  1. Creating and triggering events