iframe与主浏览器窗口之间的通信协议

许多开发人员定期需要在多个浏览器选项卡之间建立通信:具有将消息从一个发送到另一个并接收响应的功能。 这样的任务摆在我们面前。


有诸如BroadcastChannel之类的标准解决方案,但是现在浏览器的支持尚需时日 ,因此我们决定实现我们的库。 库准备就绪时,事实证明不再需要这种功能,但是出现了另一项任务:有必要在iframe和主窗口之间进行通信。


经过仔细检查,发现不能同时更改三分之二的库,您只需要稍微重构一下代码即可。 该库是一个可以处理文本数据的通讯协议。 如果可以传输文本(iframe,window.open,worker,浏览器标签,WebSocket),则可以在所有情况下使用它。


如何运作


目前,该协议具有两个功能:发送消息和订阅事件。 协议中的任何消息都是带有数据的对象。 该对象的主要字段是类型字段,它告诉我们它是哪种消息。 类型字段是具有值的枚举


  • 0-发送消息
  • 1-发送请求
  • 2-收到回应。

讯息发送


发送消息并不意味着响应。 为了发送事件,我们构造一个带有字段的对象:


  • 类型 -事件类型0
  • 名称 -用户事件的名称
  • 数据 -用户数据(类似于JSON)。

当我们在另一侧收到类型 = 0字段的消息时,我们知道这是一个事件,并且有一个事件名称和数据。 剩下的就是触发事件(几乎是常规的EventEmitter模式)。


处理事件的方案:



要求提交


发送请求意味着在库内部正在生成一个请求ID,该库将等待具有该ID的响应,并且在成功响应之后,将从中删除服务字段,并将响应返回给用户。 另外,您可以设置最大响应时间。



有了请求,一切都会变得更加复杂。 要响应请求,您必须声明我们协议中可用的方法。 这是使用registerRequestHandler方法完成的。 它接受将响应的请求的名称,以及返回响应的函数。 要创建请求,我们需要一个id ,通常,您可以使用timestamp ,但是调试起来非常不便。 因此,这是发送请求类的ID +请求的序列号+字符串常量。 接下来,我们构造一个对象,其字段ID为id类型 -值为1, name-请求的名称,data-用户数据(类似于JSON)。


收到请求后,我们检查是否有API可以响应此请求,如果没有API,则返回错误。 如果有一个API,我们从registerRequestHandler返回函数的结果,并带有相应的请求名称。


为响应形成的对象具有类型字段-值为2, id-我们正在响应的消息的id,status-表示此响应是否是错误的字段(如果没有API,或者在用户出口中发生错误,或者用户返回了被拒绝的承诺,其他错误(序列化)), 内容 -响应数据。


因此,我们描述了协议本身的操作,该协议实现了Bus类,但没有描述如何实际发送和接收消息。 为此,我们需要适配器-具有3个方法的类:


  • send-实际上负责发送消息的方法
  • addListener-订阅事件的方法
  • destroy-销毁Bus时销毁订阅。

转接器 协议实施。


要开始所有这一切,目前仅准备好用于iframe /窗口的适配器。 它适用于postMessageaddEventListener 。 这里的一切都非常简单:您需要使用正确的来源将消息发送到postMessage ,并通过事件“ message”上的addEventListener侦听消息。


我们遇到的小妙处:


  • 您应该始终在您的窗口上收听答案,并将其发送给其他人(iframe,opener,parent,worker等)。
    事实是,当您尝试在其他人的窗口上收听消息时,如果来源不同于当前消息,则会发生错误。
  • 收到消息后,请确保将其发送给您(在窗口中触发了来自分析的一堆消息
    WebStrom(如果使用的话)是外国iframe,因此您应确保该事件在我们的协议中以及对我们而言)。
  • 您不能使用Window的实例返回Promise ,因为Promise在返回结果时会尝试检查结果是否具有then方法,并且如果您无权访问该窗口(例如,起源不同的窗口),则会发生错误(尽管并非在所有浏览器中) ) 为避免此问题,只需将窗口包装在一个对象中,并在Promise中放入一个具有所需窗口链接的对象。

使用示例:


可以使用您喜欢的软件包管理器来安装该库- @ wave / Waves-browser-bus


要与iframe建立双向通信,只需编写代码:


import { Bus, WindowAdapter } from '@waves/waves-browser-bus'; const url = 'https://some-iframe-content-url.com'; const iframe = document.createElement('iframe'); WindowAdapter.createSimpleWindowAdapter(iframe).then(adapter => { const bus = new Bus(adapter); bus.once('ready', () => { //    iframe }); }); iframe.src = url; //   url   WindowAdapter.createSimpleWindowAdapter document.body.appendChild(iframe); 

在iframe中:


 import { Bus, WindowAdapter } from '@waves/waves-browser-bus'; WindowAdapter.createSimpleWindowAdapter().then(adapter => { const bus = new Bus(adapter); bus.dispatchEvent('ready', null); //      }); 

接下来是什么?


事实证明,可以在任何情况下使用的灵活通用协议。
现在,我计划将适配器与协议分开,并将它们放在单独的npm包中,添加用于处理worker和浏览器选项卡的适配器。 我想编写实现其他任何需求的协议的适配器,它尽可能地简单。


如果您希望加入有关库功能的开发或想法,欢迎您访问存储库

Source: https://habr.com/ru/post/zh-CN455942/


All Articles