什么是微前端

微前端的概念是由ThoughtWorks在2016年提出来的,它借鉴了服务端微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用。

微前端原理与选型 - 图1

核心思想

  • 技术栈无关,每个微应用都可以选择适合自己团队的框架,做到不同微应用技术框架都能够整合到主应用。
  • 子应用间状态隔离,互不影响。包括:CSS隔离,避免全局样式污染;js运行环境隔离,避免冲突,同时还需要有可靠的通信机制来进行应用间的通信。
  • 模块间应该做到高内聚低耦合,每个模块专注于自己特定领域业务,然后共同组成一个整体展现给用户,降低用户使用系统时的切换成本。
  • 应用间独立开发、部署,每个应用可以由不同团队迭代维护,提升开发和部署效率。

为什么不直接用iframe

如果不考虑体验问题,iframe几乎是完美的微前端解决方案,天然的js、css、dom隔离。但也正因为它的隔离性难以突破,极大增加了开发难度。

  1. UI完全隔离,DOM结构不共享。例如子应用难以实现类似全局弹窗的功能。
  2. 宿主环境与iframe环境全局上下文完全隔离,内存变量不共享。造成宿主与iframe间通信复杂。
  3. 性能问题,iframe的每次加载都会触发浏览器重加载整个页面所需资源,用户体验差。
  4. url不同步,浏览器刷新后状态丢失,前进后退能力不起作用,需额外处理。

由于存在以上问题,所以微前端架构大多没有直接采用iframe方案,但是依然有部分框架会利用iframe的特性做沙箱隔离。

微前端关键技术

微前端的实施主要包含:路由管理、全局状态管理及通信、DOM隔离、沙箱隔离。

路由管理

从形式上看,微前端由主应用负责整个应用框架的渲染,随着路由切换,会触发不同微应用的加载,这就涉及到了路由管理。

中心化路由

即在主应用维护全局路由表和菜单,将微应用的路由信息合并到全局路由表中。由主应用决定加载某个具体微应用。中心化路由便于主应用集中管理路由,减少应用间潜在的路由冲突,但是也导致了微应用在新增路由时需要变更主应用。

动态路由

由微应用维护自己的路由,然后动态传递给主应用。可以是通过主应用定制接口规范,以请求方式获取各微应用路由表,也可以在各自的入口文件暴露路由信息给主应用,具体还得看框架的支持情况。

全局状态管理及通信

由于微前端概念将一块整体业务分成多个子业务模块,所以全局状态管理和通信就很有必要了。

一般来说,成熟的微前端框架本身就提供了桩体管理和通信机制,比如qiankun提供了基于actions和props状态管理和通信机制、wujie提供了基于window、eventBus的通信机制。

除此以外,对于React微前端应用,可以采用通用的React状态管理库Redux,Vue微前端可以采用Vuex来做状态管理。

DOM隔离

DOM隔离分为CSS隔离和节点隔离。

CSS同名样式间是会互相影响的,各微应用间、微应用与主应用间如果不采取策略的话很容易在命名上冲突,导致相互影响。新项目可以通过命名规范约定的方式来避免命名冲突。对于旧有项目整合,要确保应用之间样式互不影响,就需要对应用间的样式进行隔离。

shadow DOM方案

Shadow DOM允许将Shadow DOM树附加到常规的DOM树中,可以像操作正常节点一样来操作Shadow DOM,包括为整个Shadow DOM添加自己的样式,且Shadow DOM内部的元素始终不会影响到外部元素。

使用影子 DOM - Web API 接口参考 | MDN

概念

微前端原理与选型 - 图2

  • Shadow host:一个常规的DOM节点,Shadow DOM会附加到这个节点上。
  • Shadow tree:Shadow DOM内部的DOM树结构。
  • Shadow boundary:Shadow DOM影响范围。
  • Shadow root:Shadow tree根节点。

用法

  1. // 创建一个shadow host
  2. const host = document.querySelector("#host");
  3. // 创建一个shadow dom
  4. // mode: open/closed,控制能否通过ele.shadowRoot方式获取节点
  5. const shadow = host.attachShadow({ mode: "open" });
  6. const span = document.createElement("span");
  7. span.textContent = "I'm in the shadow DOM";
  8. // 为shadow dom添加shadow tree
  9. shadow.appendChild(span);
  10. // 添加样式
  11. const style = document.createElement('style');
  12. style.textContent = '.title{color:red}';
  13. shadowRoot.appendChild(style);

类似以上示例,只需要将每个微应用都包裹到各自的shadow dom中,即可保证主、微应用间节点和样式不会相互影响。

CSS Module

通过在样式后缀添加hash方式,来实现主应用和微应用间的样式隔离,需要我们在微应用工程开启css module能力。改造已有项目,工程量较大。

Scoped CSS方案

通过样式作用域与权重来达到样式隔离。具体做法是:在挂在每个子应用处,命名唯一选择器作为标识,然后给所有的子应用样式添加对应的scope。

  1. // 主应用
  2. .title {color: red};
  3. // 微应用
  4. .micro-app-1 .title {color: blue};
  5. <!-- 主应用挂载微应用处 -->
  6. <div class="micro-app-1">
  7. <!-- 微应用节点 -->
  8. </div>

通过编译的时候遍历每个微应用的样式属性,给每个样式选择器前添加微应用专属的scope,这样微应用的样式权重就会提升,且不会对主应用样式造成影响。

目前看来Shadow DOM方案更被广泛用来做样式隔离的主流方案,主要是因为实现隔离比较彻底,改造成本比较小。

JS沙箱

JS沙箱隔离目的是尽可能避免微应用运行对全局环境造成影响。

JS代码执行

控制JS执行时机

常规的js加载执行都是在script标签内完成的,我们没有办法精准控制其执行时机。

要实现沙箱,需要控制沙箱的开启和关闭,就需要精确掌握脚本的执行时机,所以就需要寻找一种合适的能手动执行代码的方法。常规的有:eval()、new Function()。这两种在不同的框架都有使用,不过后者在安全性和性能方面占优。

创建运行环境

比较通用的做法是创建个立即执行函数,如qiankun底层引用import-html-entry包完成可执行js的拼接并将其包裹在立即执行函数内部,同时指定具体运行的上下文环境。

微前端原理与选型 - 图3

wujie则利用iframe天然沙箱隔离特性,将子应用实例输入到iframe内运行。

微前端原理与选型 - 图4

微前端原理与选型 - 图5

创建沙箱方案

先看一下qiankun创建沙箱环境的代码片段,运行环境支持proxy就优先创建基于proxy的沙箱,不支持就创建快照沙箱。

微前端原理与选型 - 图6

快照沙箱SnapshotSandbox

快照沙箱核心是

  1. 维护两个变量
    1. windowSnapshot保存微应用激活时对应的window属性快照。
    2. modifyPropsMap保存微应用卸载时的已变更属性。
  2. 在active阶段遍历window的变量,保存为windowSnapshot。
  3. 在inactive(deactive)阶段再次遍历window上的变量,并和windowSnapshot做对比,将不同的属性存到modifyPropsMap变量中,并将window已变更的属性恢复到windowSnapshot的属性状态。
  4. 当再次切换微应用时,把modifyPropsMap的变量恢复回window上,实现沙箱的切换。

快照沙箱的实现机制决定,如果同时运行多个微应用,则很有可能造成window属性混乱,因此基于快照沙箱不能用于需要同时运行多个微应用的业务场景。

Proxy沙箱

ES6新增Proxy API后,可以方便地创建一个对象的代理,而不用像快照沙箱那样遍历所有window属性。

proxy方案又可以细分为

  • LegacySandbox:基于proxy方案的快照沙箱的平替方案,不能支持需同时运行多个微应用场景。
  • ProxySandbox:支持同时运行多个微应用的最终解决方案。

qiankun源码对LegacySandbox态度也是有下线计划的,因此该方案就不再讲述了。

ProxySandbox沙箱源码是比较复杂的,全面处理了各种边界情况,其核心思想即创建一个对window对象的代理:proxyWindow。然后将该对象作为上下文环境传入到微应用中运行。

可以发现:和快照沙箱相比,并没有记录属性变化和状态恢复的操作。那是因为实际运行时,所有的操作都是基于proxyWindow变量来的,并不会对window变量造成影响,自然也就没必要恢复变更操作。

都是基于proxy实现的方案,ProxySandbox基本可以替代LegacySandbox。

但是由于proxy没办法被完全polyfill,SnapshotSandbox可以替代在低版本浏览器使用。

VM沙箱

目前只有garfish使用该策略,VM沙箱使用类似于node的vm模块,通过创建一个沙箱,然后传入需要执行的代码。

iframe沙箱

目前只有wujie利用iframe来实现js的沙箱隔离。无界将子应用的 js 放置在 iframe(js-iframe)中运行,实现了应用之间 window、document、location、history的完全解耦和隔离。

同时,无界在底层采用 proxy + Object.defineproperty 的方式将 js-iframe 中对dom操作劫持代理到webcomponent shadowRoot 容器中,影响收敛在各自应用内。

主流微前端框架对比

对比

框架 single-spa qiankun wujie micro-app garfish
简介 开源社区,微前端鼻祖 蚂蚁,基于single-spa封装 腾讯,基于webComponent和iframe 京东,基于WebComponent 字节
微应用加载 核心是一种运行时协议,定义了主应用如何配置微应用,从而感知微应用的加载和卸载时机。 基于但区别与spa加载微应用方式,它采用import-html-entry方式加载微应用 基于WebComponent容器和iframe沙箱来实现微前端组件式加载。 借鉴WebComponent的思想,通过CustomElement结合自定义的shadow dom,将微前端封装成一个组件,来加载微应用。 Garfish.loadApp控制子应用的加载与销毁。
DOM隔离 无 基于shadow dom
iframe通过proxy的方法将dom劫持到shadow dom上 基于shadow dom实现 基于VM沙箱,通过劫持document.createElement和appendChild方法实现。
CSS隔离 无 支持shadow dom和scoped css 基于iframe劫持和shadow dom 默认基于scoped css,也支持shadow dom,但官方提示对React支持不好,慎用 基于shadow dom
JS隔离 无 支持快照和proxy沙箱 基于iframe 基于proxy 支持快照和proxy沙箱
状态管理与通信 无 提供了actions全局状态管理与基于props注入通信。 + props注入
+ 基于iframe同域下的window
+ 去中心化的eventBus
window.microApp上挂在dispatch、getData、setData等丰富api用于通信 提供Garfish.channel,基于EventEmitter2实现应用间通信
内置生命周期 无 完整的生命周期
beforeLoad、beforeMount、afterMount、beforeUnmount、afterUnmount
更完整的生命周期
beforeLoad、beforeMount、afterMount、beforeUnmount、afterUnmount、activated、deactived、loadError
较丰富的生命周期
created、beforemount、mounted、unmount、error
比较实用的生命周期
mount、unmount、show、hide
接入成本 高、框架过于原始,什么都要自行封装 高,生命周期、路由、静态资源路径配置、weibpack配置都需适配。 低 低 高
优点 自定义程度高 使用最广,社区活跃,很多踩坑解决方案 + 虽然CSS是基于shadow dom,但iframe通过proxy的方法将dom劫持到shadow dom上,达到彻底隔离
+ vite支持友好
使用简单,代码无入侵,不依赖其他三方库 无
坑点 框架过于原始,什么都要自行封装 + css沙箱隔离不完全,严格模式基于shadow dom,第三方组件的弹窗默认挂到body下面,这样弹窗中的自定义样式会失效,需要手动设置挂载阶段
+ 样式隔离基于scoped css,对于同名样式依然存在问题
+ 不支持vite
+ 内存开销较大,承载js的iframe是隐藏在主应用的body下面,常驻内存
+ 不同技术栈需接入不同的包版本适配
+ 样式隔离基于scoped css,对于同名样式依然存在问题
+ 浏览器兼容,WebComponent
+ vite适配成本高
+ 不同技术栈需接入不同的包版本适配

选型建议

  • 微前端主要用来解决大型项目的管理、迭代开发、部署问题,简单项目不需要
  • 对于简单嵌入进应用的场景,iframe就够了
  • 基于上面对各个微前端框架的对比
    • single-spa不建议采用
    • 基于上手和接入成本,wujie和micro-app推荐
    • qiankun上手成本高,但社区活跃,遇到问题更容易解决

参考文章

微前端时代:打造高效、灵活的前端开发体系

使用影子 DOM - Web API 接口参考 | MDN

Proxy - JavaScript | MDN

微前端01 : 乾坤的Js隔离机制原理剖析(快照沙箱、两种代理沙箱) - 掘金

你不知道的JS 沙箱隔离-腾讯云开发者社区-腾讯云

微前端的落地和治理实战-腾讯云开发者社区-腾讯云