什么是微前端

微前端的概念是由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 沙箱隔离-腾讯云开发者社区-腾讯云

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