多类型微应用的通信SDK设计思路探索

1,972 阅读6分钟

首先先祝大家1024程序员节节日快乐!

注意:本文不涉及微应用与主应用的状态共享,只考虑进行双向通信。

背景

在我们之前对现有的项目进行微前端改造时,总会有一些无法改造为微前端项目,或者涉及成本不容易改造的项目。如: ssr,jsp/php 项目等。但不管成本有多大,而我们总要用各种方式接入进来。所以在有些场景下,我们一般会将微前端的改造降级为iframe,以适应我们的需求。

但微前端的通信,与iframe的通信有着很大的区别。

通信方式的差异

由于微前端的实现容器,依然是与父项目在同一JavaScript执行环境中,所以通信也会变得非常简单。但iframe的执行环境与父项目隔离,也就提升了通信的困难。在非跨域情况下,我们也可以通过使用与微前端相似的通信方式进行通信(直接调用window.parent对象)。mdn的介绍

image.png

但在跨域情况下,浏览器的安全策略是不允许iframe采用这样直接的通信方式,而是采用postMessage的方式异步通信,且传递的数据只能被结构化克隆算法序列化。关于结构化克隆算法,可以看下面的介绍:

developer.mozilla.org/en-US/docs/…

image.png

微前端与iframe通信方式的差异区别,导致了其通信的思路也有很大差距。但无论如何,微应用下的通信方式我们要尽可能做到抹平差异。所以要设计一套通用的通信sdk,让任何支持的应用类型都可以自由接入,通信的API使用也完全一致。

适配多类型应用接入

SDK要抹平在使用层面上的差异,架构的设计也要有一定要求。

一般聚合设计思路可能只是ifelse,例如阿里某小程序SDK聚合框架的设计。这个方式是最简单直接粗暴的,也是最有效的。但实现上却不是很优雅,维护成本也会变高。这种情况可以利用设计模式中的 “适配器模式” 进行改造实现。

由于许多前端同学对 发布订阅模式生产者消费者模式 了解更多,这里我就科普一下适配器模式。举个例子:有时候,其他国家/地区的电器插头与中国大陆的电器插头是不一致的。例如在香港,电器插头一般比大陆的插头要大一些(如下图)。所以我们买回来香港的电器,是不能直接用的。需要购买一个适配器来进行转换(如下图),这就是适配器模式在现实生活中的应用。

image.png

image.png

适配器模式的作用就是解决两个软件实体间的接口不兼容问题。使用适配器模式后,两个因为接口不兼容而无法共同工作的实体可以一起工作。 适配器模式的概念其实不难理解,但在代码中如何实现呢?

我们将iframe和微前端,分为两个类分别实现。分别为 IframeChannelLocalAppChannel。两个类中分别实现各自的逻辑。IframeChannel 中有着对异步回调的逻辑,还有对message监听的处理逻辑。

image.png

image.png

但无论实现如何,IframeChannelLocalAppChannel 这两个类所暴露的api都是完全一致的。最后在sdk的入口里面,根据判断当前页面是不是iframe,选择实例化不同的适配器

image.png

这样我们利用适配器模式,我们可以为微前端和iframe单独封装不同的cannel,并通过一个统一的类将其聚合起来。这样一来,无论是微前端还是iframe的通信代码,都尽可能地不污染主体通信逻辑的实现。

不过,最佳实践和设计模式都不是一劳永逸的。依然可能会有少量的侵入性代码。我们只能尽量做到维护成本最小。

异步消息的处理

日常开发中,难免会遇到一些异步通信的场景。

例如:让主项目给你某个数据,让主项目打开个弹窗等等。但异步有一个最大的问题,就是会导致竞态。举个例子:你发送了2条异步消息出去,异步消息的返回先后顺序,是不确定的。 而如果不理顺这两条消息的先后顺序,对业务逻辑的影响是很严重的。

这种情况,在 iframe 场景更为突出。postmessage 本身就是异步的消息通信。 可能会面临着,并发2条消息出去,但分不清这两条消息的返回到底归属哪一条。针对这个问题,可以选择为每一条消息都携带一个唯一id。

image.png

在发送消息时,携带一个唯一id发出去,并利用消息id作为key,将resolve和reject存入我们的全局map中。

image.png

在给子应用发送异步结果消息时,也携带之前的唯一id回传回去。

image.png

子应用拿到id后,找到对应的异步回调函数并触发。

image.png

同时,为了所有消息都能互相掌握状态,无论是父应用给子应用发消息,还是子应用给父应用发消息,都要求对方回传一条 ACK 信息确保对方收到且完成消息处理,这个思路源于网络协议。同时利用ACK确认消息的设计,也可以做超时的处理逻辑。

image.png

消息唯一ID的设计实现

前面我们提到了,发送消息时要携带一个唯一的消息ID,以便于后面进行身份识别。消息ID的选型也有以下几种:

  • Math.random():利用随机数生成唯一ID,重复率极高,抛弃
  • UUID:重复率极低,但依然有可能性。暂缓组合考虑
  • 计数器:唯一的变量来源,理论靠谱

计数器的实现,一般是根据发送的消息ID自增的序号。由于JavaScript是单线程运行,也就不会出现类似java那样多线程语言的变量冲突问题。于是我们的消息ID生成规则,可以采用UUID + 计数器来组合处理,这样最大程度的避免ID冲突问题

其他

未来还会根据需求,可能去做跨子应用之间的通信,跨页面应用的通信。

在我们内部的项目中,已经实现了这部分的功能代码,本文只是提供了一个思路。如果有想参观代码的,欢迎加入我们团队。我的邮箱:wangshengsong@oschina.cn