likes
comments
collection
share

微前端原理浅析

作者站长头像
站长
· 阅读数 25

微前端的优势不言而喻,独立开发、独立部署、技术栈无关等等,展望未来,在设计一些大型或者有希望变成巨石应用的系统时,微前端已经成为了不得不考虑的架构方案,社区内关于快速搭建微前端应的方案举不胜举,但是作为一名coder,我们不仅要做到code,还要做到understand,秉承着知其然知其所以然的理念微前端的原理,咱们还是有必要去了解学习。

微前端方案及其概念简介

目前主流的微前端方案:

  • qiankun 基于single-spa
    • 基于single-spa基础上进行了封装,扩展了js沙箱样式隔离等
    • 提供基座模式,基座用于注册、承载、启动子应用,子应用可以为独立的前端项目
    • 提供了 单实例,多实例,应用通信,应用隔离等功能
  • microApp 基于web Components
  • iframe
  • single-spa

本文主要是对社区比较火的qiankun做一些原理上的浅析,关于qiankun微前端的搭建,官网已经描述的很详细了,且社区也有很多搭建最佳实践,这里就不多赘述。

微前端原理

若要了解一个微前端架构,可以从以下三个方面来学习: 加载、隔离、通信

加载子应用

single-spa内部是维护了一套生命周期(见本文最后single-spa生命周期图)以及app对应的状态,用于对不同阶段的子应用采取不同的执行动作,qiankun是基于single-spa,因此也遵循了single-spa的生命周期。

子应用的渲染一般有两种方案:

时机说明优点缺点
构建时基座与子应用一起打包发布容易做公共依赖以及公共组件提取主,子应用耦合
运行时主,子独立部署。运行时动态加载主,子完全解耦。技术栈无关运行时加载性能损耗

qiankun采用了运行时加载子应用,通过监听url change事件,在路由变化时,去匹配子应用进行加载挂载渲染,同时要求子应用必须暴露出三个生命周期钩子函数:

  • bootstrap:对应初始化,启动
  • mount:对应挂载渲染
  • unmount:对应卸载

其内部是通过 import-html-entry ,来加载子应用,也就是 HTML Entry 的方式,通过设置html作为资源入口,加载远程html 解析DOM,从而获取js、css等静态资源来实现微前端的渲染。

首先,当我们配置子应用的 entry 后,qiankun 会去通过 fetch 获取到子应用的 html 字符串拿到 html 字符串后,会通过一大堆正则去匹配获取 html 中对应的 js(内联、外联)、css(内联、外联)、注释、入口脚本 entry 等等。processTpl 方法会返回我们加载子应用所需要的四个组成部分:

  • template:html 模板

  • script:js 脚本(内联、外联)

  • styles:css 样式表(内联、外联)

  • entry:子应用入口 js 脚本文件 然后 会分别去获取外联js,外联css,并进行处理, 总结来讲, css全部处理成内联style,js会被一段匿名eval函数包裹,并且绑定window.proxy对象。具体流程如下

微前端原理浅析

接下来我们看下子应用在基座挂载后的DOM结构,从DOM结构中可以看出,qiankun是以HTML方式嵌入,且外联js也已经被import-html-entry处理。

微前端原理浅析

我们再看下子应用在基座挂载后的CSS,从乾坤注入的注释可以看出,外联css已经被处理成内联css嵌入。

微前端原理浅析

microApp微前端加载子应用方案

microApp借鉴了webComponent的思想,推出了类WebComponent+ HTML Entry来实现子应用加载。

子应用渲染方式特点
HTML Entry设置html作为资源入口,通过加载远程html,解析其DOM结构从而获取js、css等静态资源来实现微前端的渲染,这也是qiankun目前采用的渲染方案
WebCOmponent1. web原生组件,它有两个核心组成部分:CustomElement和ShadowDom。2. CustomElement用于创建自定义标签,ShadowDom用于创建阴影DOM,阴影DOM具有天然的样式隔离和元素隔离属性。3. 由于WebComponent是原生组件,它可以在任何框架中使用,理论上是实现微前端最优的方案。 4. WebComponent有一个无法解决的问题 - ShadowDom的兼容性非常不好,一些前端框架在ShadowDom环境下无法正常运行
类WebComponent使用CustomElement结合自定义的ShadowDom实现WebComponent基本一致的功能,从而解决兼容性问题

应用隔离

js隔离

qiankun做沙箱隔离策略主要分为三种

  1. 单实例模式下的沙箱隔离(快照沙箱)

利用es6的proxy,劫持window,本质上还是会直接操作window,激活沙箱时,还原子应用的状态,卸载时,还原主应用的状态,来实现沙箱隔离,内部存在三个状态池Map:

  • 状态池1: 存储子应用运行期间新增的全局变量池(子应用运行往window上挂载的全局变量),用于卸载子应用时,还原主应用的全局变量
  • 状态池2: 存储在子应用运行期间更新的全局变量(子应用更新了外部window的变量),用于卸载子应用时还原主应用全局变量
  • 状态池3: 存储子应用全局变量的更新(子应用初始化时的window),用于运行时切换后还原子应用的状态

微前端原理浅析

  1. 兼容模式snapshotSandBox(快照沙箱) 在不支持proxy的环境下 会降级到snapshotSandBox,在子应用激活 / 卸载时分别去通过快照的形式记录/还原状态来实现沙箱的。

微前端原理浅析

  1. 多实例下的沙箱隔离(proxy)

使用了es6的proxy的代理拦截,不会直接操作window对象,而是提供了遍历window对象的接口,为了避免子应用操作window对象从而影响主应用和其他子应用环境,qiankun会将window的属性复制到各个子应用的window副本(fakeWindow),子应用里面的环境和外面的环境完全隔离。因为这种模式不直接操作window,所以在激活和卸载时也不需要操作状态池去更新/还原主子应用的环境状态了,同时也支持了统一url下多个子应用的场景。

微前端原理浅析

样式隔离

  1. 动态样式表

对于样式隔离,qiannkun采用了HTML+ 动态样式表,来实现子应用切换的样式隔离,当子应用挂载时,qiankun会解析css(内联,外联)全部会被style包裹,插入到html模板中,当子应用A切换到子应用B的时候,会执行mounted生命周期,在该期间,子应用会执行卸载逻辑,qiankun会将样式表A全部删除,在子应用B开始挂载时,再将子应用B的样式表B挂载,这样就避免了子应用A和子应用B的样式同时存在于这个项目,实现了样式的基本隔离。

微前端原理浅析

但是在主子应用之间, 子应用多实例挂载时,这种做法,是不能做到完全隔离的,qiankun没有提供这种模式下的解决方案,不过 样式隔离是可以采用工程化手段来避免,比如css-module、ben(约定prefix)、css-in-js。

  1. 严格隔离模式 Shadow

利用了 shadow DoM,可以做到css完全隔离,但是有兼容问题。

  1. 实验性方案(动态添加前缀) runtime css transformer

qiankun实验性方案,给每一个子应用的css添加前缀

微前端原理浅析

通信

qiankun数据通信的方式有很多,比如 url,发布/订阅,props等,qiankun采用的是基于props来实现微前端应用之间的通信。

微前端原理浅析

具体实现,在基座的start函数执行时,可以将基座的数据通过props传递下去,在子应用的mounted钩子函数中,可以通过props来接收,乾坤提供了 onGlobalStateChange 和setGlobalState等API。可以在子应用中修改基座的数据变量。 从而实现了父子之间的通信交互,但是子子之间,qiankun并没有提供API级别的方案。当然这种情况也十分罕见。

预加载

预加载其实不是微前端架构独有的,为何在这里赘述,是因为当子应用也很庞大的时候,在没有预加载的前提下,试想一下,当子应用A第一次切换到子应用B时的渲染流程是怎么样的呢?先执行子应用A的卸载逻辑,然后执行子应用B的启动、加载、挂载,加载时候再去拉取子应用B的bundle,这是一个子应用,可能很庞大,并不是一个简单的页面,难免会出现耗时很久的情况,所以预加载在微前端的一定情况下是解决微前端性能的一大利器。

预加载基于requestIdleCallback实现,因此不会对基座应用和其它子应用的渲染速度造成影响,它会在浏览器空闲时间加载应用的静态资源,在应用真正被渲染时直接从缓存中获取资源并渲染。

附上single-spa生命周期以及各个阶段的含义

微前端原理浅析

各个状态的含义

状态说明下一个状态
NOT_LOADEDapp还未加载,默认状态LOAD_SOURCE_CODE
LOAD_SOURCE_CODE加载app模块中NOT_BOOTSTRAPPED、SKIP_BECAUSE_BROKEN、LOAD_ERROR
NOT_BOOTSTRAPPEDapp模块加载完成,但是还未启动(未执行app的bootstrap生命周期函数)BOOTSTRAPPING
BOOTSTRAPPING执行app的bootstrap生命周期函数中(只执行一次)SKIP_BECAUSE_BROKEN
NOT_MOUNTEDapp的bootstrap或unmount生命周期函数执行成功,等待执行mount生命周期函数(可多次执行)MOUNTING
MOUNTING执行app的mount生命周期函数中SKIP_BECAUSE_BROKEN
MOUNTEDapp的mount或update(service独有)生命周期函数执行成功,意味着此app已挂载成功,可执行Vue的$mount()或ReactDOM的render()UNMOUNTING、UPDATEING
UNMOUNTINGapp的unmount生命周期函数执行中,意味着此app正在卸载中,可执行Vue的$destory()或ReactDOM的unmountComponentAtNode()SKIP_BECAUSE_BROKEN、NOT_MOUNTED
UPDATEINGservice更新中,只有service才会有此状态,app则没有SKIP_BECAUSE_BROKEN、MOUNTED
SKIP_BECAUSE_BROKENapp变更状态时遇见错误,如果app的状态变为了SKIP_BECAUSE_BROKEN,那么app就会blocking,不会往下个状态变更
LOAD_ERROR加载错误,意味着app将无法被使用

load,mount,unmount条件

需要被加载(load)的子应用

微前端原理浅析

需要被挂载(mount)的子应用

微前端原理浅析

需要被卸载的(unmount)的子应用

微前端原理浅析

参考