微前端汇总
微前端的定义
上图是来自Micro Frontends网站对微前端的定义。意思是所谓微前端就是一种多个团队之间可以使用不同技术构建一个现代化web的技术手段以及方法策略。其中的关键字是多团队、采用不同的技术栈以及现代化的web。
上图所示为微前端的架构图,其中上层为统一共享的拼接层,主要做一些基础信息的加载,和对来自不同团队不同技术栈的客户端在运行时动态组成一个完整的SPA应用,以及生命周期的调度和事件的管理。总之,微前端是将微服务概念做了一个很好的延伸和实现。
在具体实践中,衡量一个微前端方案是否是可利用的,需要满足以下几个条件,一是技术栈无关性,不仅指子应用之间使用多个不同的框架,也指在使用同一个框架时,有可能在一个长的时间跨度下,由于框架的不兼容的升级,导致应用被锁死的情况。二是开发、发布及部署独立,要求子应用和主应用做到工程上的解耦和独立。三是应用隔离的能力,是指需要考虑如何不干扰到原来子应用的开发模式和部署模式的情况下,做好运行时的样式隔离、JS隔离以及异常隔离等。以上几点是基于工程价值方面考虑的。此外,也需要动态组合的能力,是基于产品价值方面考虑的。
按架构类型区分,常规web应用的架构类型分为两种,一种是MPA,另一种是SPA。MPA的优点在于部署简单,具备独立开发和独立部署的特性。但是,它的缺点是完成一个任务要跳到多个控制台,并且每个控制台又是重复刷新的。而SPA能极大保证多个任务之间串联的流畅性,但问题是通常一个SPA是一个技术栈的应用,很难共存多个技术栈方案的选型。SPA和MPA都是微前端方案的基础选型,但是也都存在各自的问题。
MPA (Multi-page Application) 多页面应用指的就是最传统的 HTML 网页设计,早期的网站都是这样的设计,所之称为「网页设计」。使用 MPA 在使用者浏览 Web 时会依据点击需求切换页面,浏览器会不停的重载页面 (Reload),整个操作也常感觉卡卡。如果使用这样的设计在 Web App 中,使用者体验比较差,整体流畅度扣分。但进入门槛低,简单套个 jQuery Mobile 就可以完成。
SPA (Single-page Application) 顾名思义在 Web 设计上使用单一页面,利用 JavaScript 操作 Dom 的技术实现各种应用,现今在介面上算是非常受欢迎的设计,搭配 AJAX 使得整体介面反应速度相当迅速,并且能够实现各种丰富的使用者介面。典型的 gmail 就是最好的例子。
按运行时特性区分,微前端包含两个类别,一类是单实例,另一类是多实例。单实例场景如上图中左侧,通常是一个页面级别的组合,例如一个运行时只有一个App被激活。多实例场景如上图右侧,像一个组件或者是容器级别的应用,运行时可以做到多个应用被同时激活。这两种模式都有自己适应的场景和优势。微前端架构的核心诉求是实现能支持自由组合的微前端架构,将其他的SPA应用以及其他组件级别的应用自由的组合到平台中。那么,如何选择SPA和MPA以及单实例和多实例是一个问题,我们是否能探索出一种方案,将SPA和MPA工程上的特点结合起来,同时兼顾多实例和单实例运行时的场景来实现。
技术细节上的决策
为了实现上述的方案,在技术细节上的决策需要注意以下问题:
一是如何做到子应用之间的技术无关;
二是如何设计路由和应用导入;
三是如何做到应用隔离;
四是基础应用之间资源的处理以及跨应用间通信的选择。
对于如何做到子应用之间的技术无关问题,我们是通过协议来解决的。如上图所示的方式,就可以完成子应用的导入。如果子应用接入时做了一些框架上的耦合或者依赖一个具体实现库的机制,就一定会存在与实现库版本耦合的可能,不利于整个微前端生态的统一和融合。
如上图所示是一个不与某个具体框架实现耦合的例子。
对于路由的问题,如上图所示。这样一条访问链路后,刷新当前URL通常情况下会发生什么?正常访问一个站点,经过一番操作之后,进入到站点的列表页,路由会变大很复杂,但如果是一个微前端用户,刷新一下页面会出现404的情况。解决思路是将404路由fallback到一个异步注册的子应用路由机制上。
对于应用导入方式的选择,比较常见的方案是Config Entry,如上图所示。通过在主应用中注册子应用依赖哪些JS。这种方案一目了然,但是最大的问题是ConfigEntry的方式很难描述出一个子应用真实的应用数据信息。真实的子应用会有一些title信息,依赖容器ID节点信息,渲染时会依赖节点做渲染,如果只配JS和CSS,那么很多信息是会丢失的,有可能会导致间接上的依赖。
另外一种方案是HTML Entry,如上图所示,直接配html,因为html本身就是一个完整的应用的manifest,包含依赖的信息。HTML Entry的优点是接入应用的信息可以得到完整的保留,接入应用地址只需配一次,子应用的原始开发模式得到完整保留,因为子应用接入只需要告知主应用html在哪,包括在不接入主应用时独立的打开。它的缺点是将解析的消耗留给了运行时。而Config Entry相较于HTML Entry减少了运行时的解析消耗。Config Entry的缺点是主应用需配置完整的子应用信息,包含初始DOM信息、js/css 资源地址等。
对于样式隔离问题,例如BEM,每个子应用在写样式之前要加一些前缀,做一些隔离,但是这个做法并不推荐。相对而言,CSS Module更简单高效,也更智能化,是比较推荐的方式,但是也存在着问题。而Web Components看上去很不错,但在实践过程中也会发生一些问题。
例如在Web Components渲染的流程中出现了问题,如上图所示。
解决方案为上图所示。在antd中提供了全局的API,可以提前设置好所有的弹框的container,但是也不是每个组件库都能像antd一样完成度那么高。
蚂蚁所采用的解决方案是做动态的加载和卸载样式表,如上图所示,这种方案是很有效的。
对于JS隔离,蚂蚁提出了JS Sandbox机制,如上图所示,其中bootstrap、mount及unmount生命周期是子应用需要暴露出来的,因为子应用的整个生命周期都是被主应用所管理的,所以可以在主应用中给子应用插入各种拦截的机制,也可以捕获到子应用在加载期间做了哪些全局上的修改。在unmount时,可以将全局上的副作用全部手动移除掉,同时也可以实现在重新进来时,将上次忘记卸载的副作用重建一遍,因为需要保证下次进来时能完整回复到与上次一致的上下文。
对于资源加载问题,在微前端方案中存在一个典型的问题,如果子应用比较多,就会存在之间重复依赖的场景。解决方案是在主应用中主动的依赖基础框架,然后子应用保守的将基础的依赖处理掉,但是,这个机制里存在一个问题,如果子应用中既有react15又有react16,这时主应用该如何做?蚂蚁的方案是在主应用中维护一个语义化版本的映射表,在运行时分析当前的子应用,最后可以决定真实运行时真正的消费到哪一个基础框架的版本,可以实现真正运行时的依赖系统,也能解决子应用多版本共存时依赖去从的问题,能确保最大程度的依赖复用。
对于应用之间数据共享及通信的问题,蚂蚁提出了两个原则,第一个原则是基于props以单向数据流的方式传递给子应用。第二个原则是基于浏览器原生事件做跨业务之间的通信。
在真实的生产实践中,蚂蚁总结出了几点经验及建议:兄弟节点间通信以主应用作为消息总线,不建议自己封装的Pub/Sub机制,也不推荐直接基于某一状态管理库做数据通信。