微前端
# 微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
# 微前端优点
微前端架构具备以下几个核心价值:
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时
每个微应用之间状态隔离,运行时状态不共享
# 为什么不用 iframe
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
url 不同步。浏览器刷新 iframe url 状态丢失、原有的页面丢失,跳转到默认设置好的页面、后退前进按钮无法使用。
UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
参考:https://www.yuque.com/kuitos/gky7yw/gesexv (opens new window)
# single-spa
single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案。
registerApplication 和 start 函数
# 微前端架构
# 微前端原理总结
原理总结:
微前端实现原理是 主工程在运行时获取应用配置,然后注册应用和路由,先加载主应用(菜单等),当 url 地址变化时,通过路由管理器和应用管理器动态加载对应的子应用,路由管理器是指把所有子应用的路由统一放在一个总路由里管理,应用管理器是把所有子应用打包后的实例放在一个应用管理器里,当 URL 和路由管理器中的子应用路由匹配时,通过应用管理器加载对应的子应用
组成:微前端架构由主应用和子应用两个部分组成,子应用负责具体的业务实现,主应用负责子应用的加载和卸载,即生命周期管理。
主应用需要具有
路由与子应用加载
能力
由于主应用负责调度子应用,因此主应用需要具备路由管理和资源加载能力。
路由管理:所谓路由管理,就是主应用中需要维护一个路由表,当页面路由发生变化的时候,主应用可以知道当前需要启动哪个子应用。这个路由表可以是动态的,也可以是静态的。
资源加载:
主应用就需要加载子应用的资源。通常有两种资源加载方式:
(1)JS Entry:通常将子应用的所有资源打包成一个入口文件
(2)HTML Entry:子应用构建输出的是一个 HTML 文件,主应用通过加载这个 HTML 文件完成子应用的加载。
- 启动子应用
在加载完子应用的资源以后,主应用就可以启动子应用,完成页面渲染了。那么该如何启动子应用呢?主应用需要与子应用之前制定一个接口规范,比如在 single-spa 中就指定了 bootstrap、mount、unmount 和 unload 四个方法。子应用暴露这四个方法给主应用,主应用通过这四个方法来管理子应用的声明周期。
- 另外还有 CSS 样式隔离和 JS 沙箱隔离
# 主框架与子应用集成
微前端架构模式下,子应用打包的方式,基本分为两种:
- 构建时
特点:子应用通过 Package Registry(可以是 npm package,也可以是 git tags 等其他方式)的方式,与主应用一起打包发布
优点:主应用、子应用直接可以做打包优化,如依赖共享等
缺点:子应用与主应用直接产品工具链耦合,工具链也是技术栈的一部分
字应该每次发布依赖主应用重新打包发布
- 运行时
特点:子应用通自己构建打包,主应用运行时动态加载子应用资源
优点:主应用、子应用直接完全解耦,子应用完全技术栈无关
缺点:会多出一些运行时的复杂度和 overhead
很显然,要实现真正的技术栈无关跟独立部署两个核心目标,大部分场景下我们需要使用运行时加载子应用这种方案。
# 路由与子应用加载
由于主应用负责调度子应用,因此主应用需要具备路由管理和资源加载能力。所谓路由管理,就是主应用中需要维护一个路由表,当页面路由发生变化的时候,主应用可以知道当前需要启动哪个子应用。这个路由表可以是动态的,也可以是静态的。
主应用需要加载子应用的资源。通常有两种资源加载方式:
- JS Entry
通常将子应用的所有资源打包成一个入口文件,在 single-spa 的很多样例中就使用了这种方式。
- HTML Entry
子应用构建输出的是一个 HTML 文件,主应用通过加载这个 HTML 文件完成子应用的加载
相比较而言,JS Entry 的方案限制更多一些,比如要求将图片、样式等所有资源打包成一个 JS Bundle,构建的包太大,也无法利用浏览器的并行加载能力。同时,子应用还需要与主应用约定好要挂载的节点,主应用要提前初始化好,或者子应用自行创建,避免挂载失败或者冲突。
HTML Entry 很好的避免了 JS Entry 的问题。本质上,HTML 文件充当的是应用静态资源表的角色。主应用加载了 HTML 以后,浏览器会自行下载子应用的各种资源。同时,由于构建产物是 HTML,子应用具备与独立应用开发时一致的开发体验。当然,HTML Entry 也存在缺点,比如要多一次请求,先加载了 HTML 才能知道加载哪些资源。
在加载完子应用的资源以后,主应用就可以启动子应用,完成页面渲染了。那么该如何启动子应用呢?主应用需要与子应用之前制定一个接口规范,比如在 single-spa 中就指定了 bootstrap、mount、unmount 和 unload 四个方法。子应用暴露这四个方法给主应用,主应用通过这四个方法来管理子应用的声明周期。
# 隔离
解决了路由和子应用加载的问题,理论上说我们已经实现了微前端的核心能力。但是,在实际的工程实践中,我们还需要解决很多的细节问题。其中最大的一部分就是如何做好子应用间的隔离。比如如何避免子应用间的样式冲突。
抛开现有的微前端方案,假如让我们从头开始实现一套微前端架构,将独立开发部署的各个子应用组合起来。相信大多数同学都会首先想到 iframe。其实我们就可以通过 iframe 来理解微前端架构中的种种技术细节。
iframe 自带的样式、环境隔离机制使得它具备天然的沙箱能力,但是 iframe 也有很多天然的缺陷,比如事件无法冒泡到顶层,路由跳转无法与主应用同步,与主应用通信复杂繁琐等。
我们可以参考 iframe 的设计思想,来设计如何对子应用进行隔离。一个传统的 iframe 具备四层能力:文档的加载能力、HTML 的渲染能力、独立执行 JavaScript 的能力、隔离样式的能力。
文档的加载能力和 HTML 的渲染能力在前面主应用加载子应用资源的时候,我们已经做了说明。
# 沙箱
沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。
通常,子应用在运行期间会有一些污染性的副作用产生,比如全局变量、全局事件、定时器、网络请求、localStorage、全局 Style 样式、全局 DOM 元素等。为了保证应用能够稳定的运行且互不影响,需要提供安全的运行环境,能够有效地隔离、收集、清除应用在运行期间所产生的副作用,也就是沙箱的设计目标。
有两种沙箱的设计思路。一种是快照模式,另一种是虚拟机(virtual machine)模式。
- 快照模式
所谓快照模式,就是将启动子应用之前,对当前环境打一个快照,子应用退出之后,再重新加载这个快照来恢复环境。
在实现层面,我们可以针对每一种副作用设计一个 save 方法保存当前状态,在设计一个 load 方法来加载保存的状态。
框照模式的缺陷是对操作的顺序要求非常严格,当页面有多个子应用的时候,快照沙箱就会有多个实例存在,此时不同顺序的 save 和 load 会产生问题。
- VM(虚拟机)模式
虚拟机想必大家都听说过,是一种计算机系统的仿真器,通过软件模拟具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。使用虚拟机就跟使用真实的计算机一样。
NodeJS 中也提供了 VM 模块,不过不同于传统的 VM,它并不具备虚拟机那么强的隔离性,并没有模拟完整的硬件系统,仅仅将指定代码放置了特定的上下文中编译并执行,无法用来执行不可信来源的代码。
VM 模式的沙箱,可以有效的解决子应用之间、主子应用之间各种副作用的有效隔离问题。qiankun 的沙箱模式就是 VM 模式。
node 的 vm 沙箱原理:node.js 里提供了 vm 模块,vm 模块可在 V8 虚拟机上下文中编译和运行代码,虚拟机上下文可自行配置,利用该特性做到沙盒的效果。
沙盒逃逸:
# 样式隔离
虽然说,VM 模式的沙箱可以收集子应用运行过程中产生的样式,然后在子应用卸载的时候去除样式,但是考虑到子应用的 dom 结构最终还是要并入到主应用的 dom 树中去,VM 沙箱无法避免主应用的样式干扰到子应用的样式的问题。
这时候,我们就需要借助于一些其他手段,比如在主子应用中都使用 css modules 来减少样式冲突。
Shadow Dom 如果不考虑兼容性,Shadow Dom 是子应用样式隔离的一个绝佳选择。
我们把子应用放到 Shadow Dom 中,可以原生实现子应用间的样式隔离。但是 Shadow Dom 本身也有诸多限制,很多依赖库还不支持 Shadow Dom。比如埋点检测,事件处理等。
我们这里仅是将 Shadow Dom 作为补充技术方案来进行说明。
参考:https://www.cnblogs.com/everfind/p/microfrontend.html (opens new window)
# 微前端带来的问题
- 整个产品的复杂度从代码转移到了基础设施
我们需要有一套应用注册、管理的系统,并要和现有的应用发布流程对接。同时还要围绕微前端方案构建一整套的基础工具,比如开发调试工具,埋点监控系统等。
- 增加了学习和理解成本
子应用或多或少要了解一些微前端方案的技术原理,才能带来更好的开发和产品体验。
# qiankun 详解
# qiankun 原理
qiankun 框架的编写基于两个十分重要框架,一个是 single-spa,另外一个是 import-html-entry
single-spa 帮住 qiankun 如何调度子应用
import-html-entry 提供了一种 window.fetch 方案去加载子应用的代码。
一个微前端的基座框架需要解决以下问题:
- 路由切换的分发问题。
- 主微应用的隔离问题。
- 通信问题。
# 为什么不是 single-spa
- single-spa 对微应用的侵入性太强
single-spa 采用 JS Entry 的方式接入微应用,使用 single-spa 接入微应用需要将微应用整个打包成一个 JS 文件,发布到静态资源服务器,然后在主应用中配置该 JS 文件的地址告诉 single-spa 去这个地址加载微应用。
将整个微应用打包成一个 JS 文件,常见的打包优化基本上都没了,比如:按需加载、首屏资源加载优化、css 独立打包等优化措施。
single-spa 没有样式隔离
single-spa 没有 JS 隔离
single-spa 没有资源预加载
single-spa 没有应用间通信
# qiankun 通信
参考:https://cloud.tencent.com/developer/article/1770605 (opens new window)
- Actions 通信
适用场景:比较适合业务划分清晰,应用间通信较少的微前端应用场景。
通信原理:qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:
(1) setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
(2) onGlobalStateChange:注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
(3) offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化。
官方提供的 Actions 通信方案是通过全局状态池和观察者函数进行应用间通信,该通信方式适合大部分的场景。Actions 通信方案也存在一些优缺点
优点:
(1)使用简单;
(2)官方支持性高;
(3)适合通信较少的业务场景;
缺点:
(1)子应用独立运行时,需要额外配置无 Actions 时的逻辑;
(2)子应用需要先了解状态池的细节,再进行通信;
(3)由于状态池无法跟踪,通信场景较多时,容易出现状态混乱、维护困难等问题;
- Shared 通信
适用场景:应用通信场景较多,希望子应用具备完全独立运行能力,希望主应用能够更好的管理子应用,
通信原理:Shared 通信方案的原理就是,主应用基于 redux/vuex 维护一个状态池,通过 shared 实例暴露一些方法给子应用使用。同时,子应用需要单独维护一份 shared 实例,在独立运行时使用自身的 shared 实例,在嵌入主应用时使用主应用的 shared 实例,这样就可以保证在使用和表现上的一致性。
Shared 通信方案需要自行维护状态池,这样会增加项目的复杂度。好处是可以使用市面上比较成熟的状态管理工具,如 redux、mobx,可以有更好的状态管理追踪和一些工具集。
Shared 通信方案要求父子应用都各自维护一份属于自己的 shared 实例,同样会增加项目的复杂度。好处是子应用可以完全独立于父应用运行(不依赖状态池),子应用也能以最小的改动被嵌入到其他 第三方应用 中。
Shared 通信方案也可以帮助主应用更好的管控子应用。子应用只可以通过 shared 实例来操作状态池,可以避免子应用对状态池随意操作引发的一系列问题。主应用的 Shared 相对于子应用来说是一个黑箱,子应用只需要了解 Shared 所暴露的 API 而无需关心实现细节。
优点:
(1)子应用无法随意污染主应用的状态池,只能通过主应用暴露的 shared 实例的特定方法操作状态池,从而避免状态池污染产生的问题。
(2)子应用将具备独立运行的能力
# qiankun 中的 css 沙箱隔离
- 严格沙箱
在加载子应用时,添加 strictStyleIsolation: true
属性,实现形式为将整个子应用放到 Shadow DOM 内进行嵌入,完全隔离了主子应用
特点:
(1)对主文档的 JavaScript 选择器隐身,比如 querySelector
(2)只使用 shadow tree 内部的样式,不使用主文档的样式
缺点:
子应用的弹窗、抽屉、popover 因找不到主应用的 body 会丢失,或跑到整个屏幕外(具体原因作者并未详细研究) 主应用不方便去修改子应用的样式
- 实验性沙箱
在加载子应用时,添加 experimentalStyleIsolation: true
属性,实现形式类似于 vue 中 style 标签中的 scoped 属性,qiankun 会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun-microName]
缺点:子应用的弹窗、抽屉、popover 因插入到了主应用的 body,所以导致样式丢失或应用了主应用了样式
# 沙箱隔离
qiankun 做沙箱隔离主要分为三种:
- legacySandBox 单例沙箱
legacySandBox 的本质上还是操作 window 对象,但是他会存在三个状态池,分别用于子应用卸载时还原主应用的状态和子应用加载时还原子应用的状态:
(1) addedPropsMapInSandbox: 存储在子应用运行时期间新增的全局变量,用于卸载子应用时还原主应用全局变量;
(2) modifiedPropsOriginalValueMapInSandbox:存储在子应用运行期间更新的全局变量,用于卸载子应用时还原主应用全局变量;
(3) currentUpdatedPropsValueMap:存储子应用全局变量的更新,用于运行时切换后还原子应用的状态;
缺点:legacySandBox 模式下在运行时期间仍然会污染 window
总结起来,legacySandBox 还是会操作 window 对象,但是他通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离的。
- proxySandBox 代理沙箱
proxySandBox 为了支持多实例的场景,proxySandBox 不会直接操作 window 对象。并且为了避免子应用操作或者修改主应用上诸如 window、document、location 这些重要的属性,会遍历这些属性到子应用 window 副本(fakeWindow)上
- snapshotSandBox 快照沙箱
snapshotSandBox 的原理就是在子应用激活 / 卸载时分别去通过快照的形式记录/还原状态来实现沙箱的。
总结:其中 legacySandBox、proxySandBox 是基于 Proxy API 来实现的,在不支持 Proxy API 的低版本浏览器中,会降级为 snapshotSandBox。在现版本中,legacySandBox 仅用于 singular 单实例模式,而多实例模式会使用 proxySandBox。
# 微前端问题
qiankun 是如何完善 single-spa 中留下的巨大缺口-————加载函数
qiankun 通过什么策略去加载子应用资源————window.fetch。
通过 window.fetch 去获取子应用的 js 代码
拿到了子应用的 js 代码字符串之后,把它进行包装处理。把代码包裹在了一个立即执行函数中,通过参数的形式改变了它的 window 环境,变成了沙箱环境。
最后通过 eval()去执行立即执行函数,正式去执行我们的子应用的 js 代码,去渲染出整个子应用。
qiankun 如何隔离子应用的 js 的全局环境————通过沙箱。
沙箱的隔离原理是什么
在支持 proxy 中有一个代理对象,子应用优先访问到了代理对象,如果代理对象没有的值再从 window 中获取。如果不支持 proxy,那么通过快照,缓存,复原的形式解决污染问题。
- qiankun 如何隔离 css 环境
沙箱可以保证子应用之间的样式隔离
qiankun 自带的 css 沙箱保证主应用和子应用之间的样式隔离。
css 严格沙箱:shadowDOM ,配置strictStyleIsolation: true
;
css 实验性沙箱:加上选择器隔离。qiankun 会自动为子应用所有的样式增加后缀标签,如: div[data-qiankun-microName]
,配置:experimentalStyleIsolation: true
- qiankun 如何获得子应用生命周期函数
export 存储在对象中,然后解构出来。
- qiankun 如何该改变子应用的 window 环境
通过立即执行函数,传入 window.proxy 为参数,改变 window 环境。
参考:微前端 qiankun 原理学习 (opens new window)
万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇 (opens new window)