微前端原理(篇二:无界)
#微前端
目录
- 1. 总结
- 2. 定义
- 3. 一个完善的的微前端框架应该具备哪些能力呢?
- 4. 使用微前端有什么收益呢
- 5. iframe 方案
- 6. single-spa 与 乾坤方案
- 7. 无界方案
- 8. 常见问题
1. 总结
- 微前端框架具备的能力
- 加载、卸载、切换应用能力
- 独立运行、环境隔离能力
- 路由状态保持能力
- 应用间通讯能力
- 微前端收益
- 技术栈无关
- 独立开发、独立部署
- 增量升级 → 技术升级 或 渐进式重构
- 独立运行时
- iframe 方案
- 优点:简单、天然隔离(js/css/dom)
- 缺点:隔离太完美
- 路由丢失
- dom 隔离严重,比如弹框问题
- 通讯困难
- 性能,比如白屏
- single-spa 与 乾坤方案
- single-spa原理
- ① 预先注册子应用(激活路由、子应用资源、生命周期函数)
- ② 监听路由 → 匹配激活路由→ 子应用 → 顺序调用生命周期 → 渲染
- 乾坤
- 入口 entry 、container 等
js沙箱SnapshotSandbox、LegacySandbox、ProxySandbox三套渐进增强方案
css沙箱做了两套strictStyleIsolation、experimentalStyleIsolation两套适用不同场景的方案
- 资源预加载能力
- 预先子应用
html、js、css资源缓存下来,加快子应用的打开速度
- 预先子应用
- 缺点
- 无法同时激活多个子应用、不支持子应用保活
css沙箱无法绝对的隔离js沙箱在某些场景下执行性能下降严重- 无法支持
vite等ESM脚本运行
- single-spa原理
- 无界方案
- 好处
- 组件方式来使用微前端
- 不需要改造、不用注册、直接使用无界组件即可
- 可同时激活多个子应用
- 天然 JS 沙箱
- 应用切换或销毁时,不需要做任何清理工作
- 组件方式来使用微前端
- 路由同步
- 劫持
iframe的history.pushState和history.replaceState- 然后将子应用的
url同步到主应用的query参数上
- 然后将子应用的
- 劫持
- iframe 连接机制和 css 沙箱机制
- 子应用的
JS 实例在iframe - 真正 DOM&CSS 结构在
webcomponent - 所以需要通过代理
iframe的document到webcomponent,可以实现两者的互联- 将
document的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body全部代理到webcomponent
- 将
- 保活原理
- 当子应用发生切换,
iframe保留下来,子应用的容器可能销毁,但webcomponent依然可以选择保留,这样等应用切换回来将webcomponent再挂载回容器上,子应用可以获得类似vue的keep-alive的能力.
- 当子应用发生切换,
- 好处
- 天然 css 沙箱,子应用不用做任何修改
- 天然适配弹框
- 子应用保活
- **完整的 DOM 结构
- **
webcomponent保留了子应用完整的html结构,样式和结构完全对应,子应用不用做任何修改
- **
- 子应用的
- 通信机制
- 无界提供了
EventBus实例 - window.parent 通信机制:子应用
iframe沙箱和主应用同源 - props 注入机制:子应用通过
$wujie.props可以轻松拿到主应用注入的数据
- 无界提供了
- 整体优势
- 具备同时激活多应用
- 组件式使用:无需注册,更无需路由适配,在组件内使用,跟随组件装载、卸载
- 应用级别的 keep-alive 保活
- 天然隔离,纯净无污染
- 接入成本低
- 插件体系:
- 无界的插件体系主要是方便用户在运行时去修改子应用代码从而避免去改动仓库代码
- 比如
- js loader
- html-loader
- 无界提供插件在运行时对子应用的 html 文本进行修改
- 应用共享
- 比如主应用使用到了
lodash,子应用 A 也使用到了相同版本的lodash 
- 但:
- 应用共享原理是主应用和子应用运行iframe沙箱同域可以共享内存,对于组件库这样有副作用的第三方包,可能无法共享
- 比如主应用使用到了
- 好处
- 坑
- MonacoEditor 认为
shadowRoot一定在document.body 内部- 而无界子应用
document.body在shadowRoot 内部 - 这导致 MonacoEditor 认为编辑器不在 shadowRoot 内
- 而无界子应用
- MonacoEditor 认为
源于无界官网,技术选型介绍说的特别好,对微前端技术原理理解有帮助!
2. 定义
通俗来说,就是在一个web应用中可以独立的运行另一个web应用
3. 一个完善的的微前端框架应该具备哪些能力呢?
- 子应用的加载和卸载能力
- 页面需要从一个子应用切换到另一个子应用,框架必须具备加载、渲染、切换的能力
- 子应用独立运行的能力
- 子应用运行会污染全局的 window 对象,样式会污染其他应用,必须有效的隔离起来
- 子应用路由状态保持能力
- 激活子应用后,浏览器刷新、前进、后退子应用的路由都应该可以正常工作
- 应用间通信的能力
- 应用间可以方便、快捷的通信
4. 使用微前端有什么收益呢
- 技术栈无关
- 主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署
- 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级
- 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,
- 而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时
- 每个微应用之间状态隔离,运行时状态不共享
可能有人会有疑问直接使用
iframe不就可以做到吗?
5. iframe 方案
采用iframe的方案确实可以做到,而且优点非常明显
5.1. 优点
- 非常简单,使用没有任何心智负担
web应用隔离的非常完美,无论是js、css、dom都完全隔离开来
5.2. 缺点
由于其隔离的太完美导致缺点也非常明显
- 路由状态丢失,刷新一下,
iframe的url状态就丢失了 dom割裂严重,弹窗只能在iframe内部展示,无法覆盖全局web应用之间通信非常困难- 每次打开白屏时间太长,对于SPA 应用来说无法接受
6. single-spa 与 乾坤方案
- single-spa是一个目前主流的微前端技术方案,其主要实现思路:
- 预先注册子应用(激活路由、子应用资源、生命周期函数)
- 监听路由的变化,匹配到了激活的路由则加载子应用资源
- 顺序调用生命周期函数并最终渲染到容器
- 乾坤微前端架构则进一步对
single-spa方案进行完善,主要的完善点:- 子应用资源由 js 列表修改进为一个
url,大大减轻注册子应用的复杂度- 即,就是一个
入口 index.html
- 即,就是一个
- 实现应用隔离
js隔离方案 (window工厂)css隔离方案 (类vue的scoped)
- 增加资源预加载能力
- 预先子应用
html、js、css资源缓存下来,加快子应用的打开速度
- 预先子应用
- 子应用资源由 js 列表修改进为一个
6.1. 优点
- 监听路由自动的加载、卸载当前路由对应的子应用
- 完备的沙箱方案
js沙箱做了SnapshotSandbox、LegacySandbox、ProxySandbox三套渐进增强方案
css沙箱做了两套strictStyleIsolation、experimentalStyleIsolation两套适用不同场景的方案- 路由保持,浏览器刷新、前进、后退,都可以作用到子应用
- 应用间通信简单,全局注入
6.2. 缺点
- 基于路由匹配,无法同时激活多个子应用,也不支持子应用保活
- 改造成本较大
- 从
webpack、代码、路由等等都要做一系列的适配
- 从
css沙箱无法绝对的隔离js沙箱在某些场景下执行性能下降严重- 无法支持
vite等ESM脚本运行
7. 无界方案
在乾坤的
issue中一个议题非常有意思,有个开发者提出能否利用iframe来实现js沙箱能力,这个idea启发了无界方案,下面详细介绍
7.1. 应用加载机制和 js 沙箱机制
- 将子应用的
js注入主应用同域的iframe中运行iframe是一个原生的window沙箱,内部有完整的history和location接口- 子应用实例
instance运行在iframe中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。
采用这种方式的收益
- 组件方式来使用微前端
- 不用注册,不用改造路由,直接使用无界组件,化繁为简
- 一个页面可以同时激活多个子应用
- 子应用采用 iframe 的路由,不用关心路由占用的问题
- 天然 js 沙箱,不会污染主应用环境
- 不用修改主应用
window任何属性,只在iframe内部进行修改
- 不用修改主应用
- 应用切换没有清理成本
- 由于不污染主应用,子应用销毁也无需做任何清理工作
7.2. 路由同步机制
- 在
iframe内部进行history.pushState- 浏览器会自动的在joint session history中添加
iframe的session-history - 浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用
- 浏览器会自动的在joint session history中添加
- 劫持
iframe的history.pushState和history.replaceState,- 就可以将子应用的
url同步到主应用的query参数上 - 当刷新浏览器初始化
iframe时,读回子应用的url并使用iframe的history.replaceState进行同步
- 就可以将子应用的
采用这种方式的收益
- 浏览器刷新、前进、后退都可以作用到子应用
- 实现成本低,无需复杂的监听来处理同步问题
- 多应用同时激活时也能保持路由同步
7.3. iframe 连接机制和 css 沙箱机制
总结就是:
- 子应用的
JS 实例在iframe - 真正 DOM&CSS 结构在 webcomponent
- 所以需要通过代理
iframe的document到webcomponent,可以实现两者的互联
-
无界采用webcomponent来实现页面的样式隔离,无界会创建一个
wujie自定义元素,然后将子应用的完整结构渲染在内部- 子应用的实例
instance在iframe内运行dom在主应用容器下的webcomponent内- 通过代理
iframe的document到webcomponent,可以实现两者的互联。- 将
document的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body全部代理到webcomponent
- 将
- 这样
instance和webcomponent就精准的链接起来。
- 子应用的实例
-
当子应用发生切换,
iframe保留下来,子应用的容器可能销毁,但webcomponent依然可以选择保留,这样等应用切换回来将webcomponent再挂载回容器上,子应用可以获得类似vue的keep-alive的能力.
采用这种方式我们可以获得
- 天然 css 沙箱
- 直接物理隔离,样式隔离子应用不用做任何修改
- **天然适配弹窗问题
document.body的appendChild或者insertBefore会代理直接插入到webcomponent,子应用不用做任何改造
- 子应用保活
- 子应用保留
iframe和webcomponent,应用内部的state不会丢失
- 子应用保留
- 完整的 DOM 结构
- **
webcomponent保留了子应用完整的html结构,样式和结构完全对应,子应用不用做任何修改
- **
7.4. 通信机制
承载子应用的iframe和主应用是同域的,所以主、子应用天然就可以很好的进行通信,在无界我们提供三种通信方式
- props 注入机制
- 子应用通过
$wujie.props可以轻松拿到主应用注入的数据
- 子应用通过
- window.parent 通信机制
- 子应用
iframe沙箱和主应用同源,子应用可以直接通过window.parent和主应用通信
- 子应用
- 去中心化的通信机制
- 无界提供了
EventBus实例,注入到主应用和子应用,所有的应用可以去中心化的进行通信
- 无界提供了
7.5. 优势
通过上面原理的阐述,我们可以得出无界微前端框架的几点优势:
- 多应用同时激活在线
- 框架具备同时激活多应用,并保持这些应用路由同步的能力
- 组件式的使用方式
- 无需注册,更无需路由适配,在组件内使用,跟随组件装载、卸载
- 应用级别的 keep-alive
- 纯净无污染
- 无界利用
iframe和webcomponent来搭建天然的js隔离沙箱和css隔离沙箱 - 利用
iframe的history和主应用的history在同一个top-level browsing context来搭建天然的路由同步机制 - 副作用局限在沙箱内部,子应用切换无需任何清理工作,没有额外的切换成本
- 无界利用
- 性能和体积兼具
- 子应用执行性能和原生一致,子应用实例
instance运行在iframe的window上下文中,- 避免
with(proxyWindow){code}这样指定代码执行上下文导致的性能下降 - 但是多了实例化
iframe的一次性的开销,可以通过 preload 提前实例化
- 避免
- 子应用执行性能和原生一致,子应用实例
- 体积比较轻量,借助
iframe和webcomponent来实现沙箱,有效的减小了代码量 - 开箱即用
- 不管是样式的兼容、路由的处理、弹窗的处理、热更新的加载,子应用完成接入即可开箱即用无需额外处理,应用接入成本也极低
8. 常见问题
MonacoEditor 其实已经考虑到了 shadowRoot 的情况,但是 MonacoEditor 和 无界没有兼容的地方在于
- MonacoEditor 认为
shadowRoot一定在document.body 内部 - 而无界子应用
document.body在shadowRoot 内部 - 这导致 MonacoEditor 认为编辑器不在 shadowRoot 内

https://github.com/Tencent/wujie/issues/205