vivo 互联网前端团队- Jiang Zuohan

一、背景

VAPD是一款专为团队协作办公场景设计的项目管理工具,实践敏捷开发与持续交付,以「项目」为核心,融合需求、任务、缺陷等应用,使用敏捷迭代、小步快跑的方式进行开发及质量跟踪,简化工作流程,帮助团队快速迭代并高效完成产品开发交付。

但早期VAPD以“一切皆可配置”的设计理念开发运行了两年,整个前端代码复杂混乱,组件庞大(需要支持多种配置),状态混乱,前端代码打包出来有50M之巨。这个项目难以为继,bug多、维护困难、新增功能处处受限,总之产品不满意、测试不满意、用户不满意。

 

因此改版是必然的选择,而改版的要求就是不能耽误用户继续使用,必须保证网站可用、逐步更新,因此微前端是必然的选择。

VAPD改版思路就是:

  1. 使用微前端框架,未改版部分作为子应用存在,继续为用户服务;

  2. 将项目模块制定系统应用,并逐个改版,降低项目复杂度;

  3. 逐步舍弃旧项目代码,将功能转移到新项目中,提升项目整体性能,提高代码可维护性。

二、什么是微前端

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. —— Micro Frontends.

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用,各个前端应用独立开发、独立部署。

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

现行最出名的微前端是阿里的qiankun,qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

但我尝试试用qiankun后发现qiankun 的npm包常常滞后于qiankun源码,有些issue解决了但还要等待其发版;首次加载子应用页面出现抖动;子应用更新后报 ChunkLoadError 问题 。 

同时它存在微前端框架常有的性能问题及冲突问题:

1、加载慢

基本上所有的微前端框架都需要先加载父框架,再加载子应用,都要经历如下图的流程。整个流程是串行的,相同流程需要走两遍,也就比普通的非微前端框架要慢1倍左右,直接影响了用户体验。

 

 2、切换慢,每次切换都要重新走一次流程

微前端框架中不同子应用切换,需要销毁当前子应用,然后加载其他子应用。子应用又需要进行“下载html –> 下载javascript文件 –> 运行javascript代码 –> 请求服务端数据 –> 页面展示”整个流程,导致微前端框架切换应用卡顿不流畅。

 3、容易出现样式冲突,不同子应用容易影响

传统微前端框架中所有子应用都在同一上下文中,如出现全局样式、全局监听、全局变量,便会对其他子应用有影响,容易出现样式冲突,内存泄漏,或者不可预知的bug。

三、Why Not Iframe

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、JS隔离这类问题统统都能被完美解决。

那为啥不使用iframe呢?

qiankun 团队也给出了原因:看这里 Why Not Iframe

总结起来就是:

  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。

  • UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..

  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。

  • 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。

四、擎天框架设计

4.1 设计理论

基于以上微前端缺陷,设计新型的微前端框架需要满足以下要求:

  • 快 —— 页面加载速度、应用切换速度是前端极致的追求,用户操作的响应速度永远是第一体验。

  • 完全隔离 —— 不同子应用完全隔离,只在子应用项目初始化时设置一次,之后不需要在后续开发中及后期维护中考虑,降低开发的心智负担。

4.2 架构设计

擎天框架架构分为三层,分别是用户入口(浏览器地址栏),主应用层以及子应用集合层。

(1)浏览器(地址栏)是用户入口,用户通过输入URL进入该系统,同时浏览器地址栏URL根据应用页面进行变化,方便用户复制分享二次进入。

(2)主应用是擎天框架的关键部分,主要是由以下两部分组成:

  • 路由引擎:实现浏览器地址栏与子应用展示隐藏的协调控制,用来控制用户第一次进入展示某个应用,当用户切换页面时需同步浏览器地址栏(方便用户复制,再次进入同一页面),并根据页面URL展示隐藏子应用。

  • 数据共享引擎:实现子应用间的数据共享,保证各个应用间数据统一,如登录信息,用户信息等。用户在某个应用修改共享数据后,会同步到数据共享引擎,再分发给其他应用,从而保证共享数据一致。

(3)子应用集合层

该层为系统提前设置好的子应用集合,子应用由全屏iframe加载,同一时刻仅有一个子应用处于可视状态,其他子应用隐藏。当需要应用切换时,隐藏当前应用,显示需要展示的应用,瞬间切换。

子应用响应擎天的路由引擎及数据共享引擎,做到实时的路由同步与数据同步,保证整个微前端项目路由及数据统一。

五、擎天框架实现

擎天框架突破了 iframe UI不同步、URL不同步、数据不共享以及加载慢等问题,并将iframe作为页面容器存在,在实现硬隔离的同时做到了子应用瞬间切换,解决了微前端框架一直以来的通病,从而实现单应用级别的操作体验。

5.1全屏iframe、共同组件

解决问题:UI 不同步

全屏iframe是指整个浏览器窗口全部给子应用iframe,子应用接管浏览器所有功能,无论是全局拖动、全部蒙层、resizie等,由此UI不同步的问题便不存在了。

但不同应用有个相同的公用部分,因此需要把公共部分做成统一组件,发到npm包中,在每一个应用中引入就行。

 

5.2 父子应用异步加载

解决问题:加载慢

在前端静态页面中预留子应用dom节点,如下图 所示,每一个div有唯一id,代表一个预制子应用。同时该id对应子应用路由pathname,如New对应/New/*路由,即以New开头的路由全部对应id为”New”的子应用。

当用户进入页面后,父框架拿到浏览器url,并获取到pathname,从而知道需要首先加载那个子应用。并且直接创建iframe,并直接挂到对应的dom节点中,父应用和子应用异步加载。

加载完首个子应用后,开始加载其他子应用,并使用display: none将它们隐藏到页面dom结构中。最终dom节点如下图所示。

5.3 子应用iframe瞬间切换

解决问题:子应用切换卡顿

用户进行多个子应用切换时,擎天框架监听浏览器url地址,如pathname从/New/*变成/Web/*,则将/New/*对应的子应用iframe隐藏起来,将/Web/对应的子应用iframe显示出来,实现应用的瞬间切换。

用户可视范围内只能看一个应用,切换时仅仅是将New应用隐藏不可见,Web应用页面可见。

 

 

5.4 路由引擎,同步切换

解决方案:URL不同步

受vue2中数组方法(如push、shift)响应式处理的启发,擎天对前端路由框架进行特殊处理,重写了vue-router的push、replace等方法,当监听到子应用使用以上方法进行路由切换时,会同步到父框架进行操作。

父子应用简单配置后,父子路由同步便做好了。路由切换分为单应用以及多应用间路由切换。

(1)单应用路由切换

单应用子iframe路由切换,如/New/b 切换到/New/c,其pathname结构一致,是单应用的路由切换。子应用先切换路由,随后发送syncRoute给父框架,父框架使用replace方法改变浏览器地址栏即可,具体流程如下图所示。

 

 

(2)多应用间路由切换

如/New/b 切换到/Web/c,其pathname结构不一致。子应用先切换路由,随后发送syncRoute消息给父框架,父框架使用replace方法改变浏览器地址栏,同时将应用A隐藏起来,并发送消息到子应用B中。子应用B获得消息后修改其本身路由。

5.5 数据共享

解决问题:内存变量不共享

父应用将需要共享的数据放到store中,并使用syncStore进行注册。

子应用通过类vuex似的 mapGlobalState 方法可以获取父应用store中数据,同时该数据为响应式,数据变更可触发UI重新渲染。

通过mapGlobalMutations获取修改数据的方法,通过该方法可以修改父应用store数据,修改成功后擎天共享数据引擎将修改好的数据分发给各个子应用,保证共享数据一致。

 

其内部逻辑是:

父应用将需要共享的数据放到store中,并暴露getTopStore(获取store数据)、syncTopMutation(更新store数据)、syncTopStore(分发store数据)等接口。

系统加载时子应用通过getTopStore方法可以获取store中数据,并赋值到子应用$pstore中,从而获得数据响应式等能力。

当某个子应用需要修改共享数据时,调用syncTopMutation方法进行修改,修改成功后擎天通过syncTopStore分发给各个子应用,保证共享数据一致。

六、总结

擎天基于全屏Iframe与搭建公共组件等方式,解决了iframe UI不同步的难题,并充分发挥了iframe作为页面容器的优势,实现了父子应用异步加载、子应用瞬间切换的能力,从而达到单应用项目的体验。