Appearance
微前端
哪些应用需要拆分成微前端?
- 技术:技术⽼旧,应用随着项目迭代越来越庞大,耦合度升高,以致缺乏灵活性,难以维护
- 团队协作:解决组织和团队间协作带来的工程问题
- 业务:用户喜欢聚合、一体化的应用,可以在既不重写原有系统的基础之下,又可以抽出人力来开发新的业务
❌ 不适合拆分为微前端的场景:
- 简单小型项目:业务逻辑简单、模块少的项目(如单页应用),拆分微前端会增加架构复杂度和维护成本,得不偿失。
- 强耦合的功能模块:模块间存在高频数据交互或共享复杂状态(如实时协同编辑功能),拆分后会导致通信成本过高,影响性能。
- 对性能敏感的场景:微前端涉及资源加载、渲染调度等额外开销,对性能要求极高的场景(如高频交互的实时数据面板)需谨慎评估。
- 团队规模小或技术能力不足:微前端需要团队掌握路由劫持、沙箱隔离、跨框架通信等技术,若团队经验不足,可能导致开发效率下降。
优点:
- 技术栈⽆关
- 便于业务拆解
- 子应用可独立测试、部署,不影响主应用稳定性
缺点:
- 架构复杂,维护成本⾼
- 性能开销与加载延迟
- 样式与交互统一成本高
- 挑战:
- 不同技术栈的组件库(如 Vue 的 Element UI 与 React 的 Ant Design)样式难以统一。
- 全局样式管理(如主题、字体)需跨应用协调,容易出现碎片化。
- 解决方案:
- 使用跨框架组件库(如 Element Plus、Ant Design)或统一设计系统(如 Figma 规范)。
- 通过 CSS 变量或 CSS-in-JS 统一主题配置。
- 挑战:
- 状态管理与通信复杂性
- 挑战:
- 子应用与主应用、子应用之间的通信需制定标准化协议(如事件总线、全局状态),否则易导致数据不一致。
- 避免强依赖主应用的状态管理库(如 Vuex),需保持子应用独立性。
- 最佳实践:
- 定义明确的通信接口(如 onLogin、onLogout 事件)。
- 使用轻量级状态管理工具(如 Redux Toolkit、Pinia)或浏览器 API(localStorage、CustomEvent)。
- 定义标准化通信接口(如 parent.postMessage),避免直接引用子应用内部实现。
- 挑战:
- 开发调试体验不好
- 版本,依赖的管理复杂
微前端带来的问题
- 跨框架通信障碍
- 问题:
- 不同技术栈(如 Vue 与 React)间的事件传递、状态共享需特殊处理。
- 子应用间通信可能导致强耦合,违反微前端隔离原则。
- 解决方案:
- 使用发布订阅,绑定在 window 上。
- 定义标准化通信接口(如 parent.postMessage),避免直接引用子应用内部实现。
- 问题:
- 加载性能下降
- 问题:
- 微前端架构需额外加载框架代码、路由配置等,导致首屏加载时间增加。
- 子应用资源分散,HTTP 请求数增多(尤其在未合并资源时)。
- 解决方案:
- 按需加载子应用(如访问时才加载),避免一次性加载所有资源。
- 使用 CDN 加速静态资源,并配置 HTTP/2 减少请求开销。
- 问题:
- 样式冲突与覆盖
- 问题:
- 不同子应用的 CSS 类名可能冲突(如都定义了 .button-primary)。
- 全局样式(如 body 样式)难以统一管理,可能被子应用意外修改。
- 解决方案:
- 使用 CSS Modules、Shadow DOM 或 CSS-in-JS 实现样式隔离。
- 通过命名空间约定(如
.app1-*、.app2-*
)避免类名冲突。
- 问题:
为什么选择微前端,微前端的缺点(微前端性能一定就要比 spa 好吗),在性能上会有损失怎么做的
🤔 为什么选择 qiankun 微前端
项目公司的项目是个后台管理,在之前是多个独立部分的后台系统,后面要求整合起来,并且大部分采用的是 antd pro 进行开发的,所以项目采用 qiankun
进行微前端合并。
🤔 微前端的缺点(是否一定比 SPA 性能差)?
微前端在某些场景下可能会比单体 SPA 的性能略差,但在特定业务需求下其优势更为重要。以下是关键点:
❌ 性能劣势
- 加载延迟:子应用资源需要远程加载,首次渲染可能存在延迟。
- 通信开销:跨应用通信可能带来额外性能损耗。
- 重复依赖:不同子应用可能引入相同的库(如 React 或 Vue),导致资源冗余。
- 复杂性增加:沙箱机制、路由劫持等会增加运行时开销。
✅ 性能优化手段
- 按需加载:通过 activeRule 控制仅加载当前需要的子应用。
- 模块联邦(Module Federation):共享公共依赖(如 React、Vue),减少重复加载。
- CDN 加速:利用 CDN 缓存静态资源,缩短加载时间。
- 懒加载与缓存策略:对非核心功能进行懒加载,结合浏览器缓存或 Service Worker 提升二次加载速度。
- CSS 模块化与隔离:使用 CSS Modules、Shadow DOM 避免样式冲突,提升渲染效率。
- 轻量级通信机制:采用 CustomEvent、postMessage 等方式实现高效通信。
🤔 在性能上会有损失怎么做的
虽然微前端存在一定的性能开销,但可以通过以下策略来缓解:
问题 | 解决方案 |
---|---|
子应用加载慢 | 使用懒加载、按需加载,结合 Webpack 动态导入 |
JS/CSS 冲突 | 引入沙箱机制(JS 沙箱、CSS-in-JS)、命名空间约定 |
资源重复 | 利用 Module Federation 共享基础库 |
通信复杂 | 使用标准化事件总线(如全局事件中心)或 CustomEvent |
首屏加载慢 | 增加预加载策略(如 prefetch、preload),配合骨架屏 |
微前端业务遇到哪些问题怎么解决
1. 主子应用样式污染
qiankun 提供的样式隔离方案不好用,react、vue 有自己的样式隔离方案
在子应用构建时,使用 postcss
插件,给子应用添加上特有的前缀(可以启动两个子应用项目,用来做样式对照,一个是单独运行的,一个是添加前缀运行到 qiankun 中的)
2. 应用间通信
- localStorage/sessionStorage
- 通过路由参数共享
- 官方提供的 props
- 官方提供的 actions(
const actions = initGlobalState({})
) - 主应用使用 vuex 或 redux 管理状态,通过
props
传递给子应用 - 使用发布订阅,将发布订阅绑定在
window
上
3. 路由跳转
在子应用中是没有办法通过 <router-link>
或者用 router.push/router.replace
直接跳转的,因为这个 router
是子应用的路由,所有的跳转都会基于子应用的 base 。当然了写 <a>
链接可以跳转过去,但是会刷新页面,用户体验并不好。
这里可以采用以下两种方式:
- 将主应用的路由实例通过 props 传给子应用,子应用用这个路由实例跳转
- 路由模式为 history 模式时,通过
history.pushState()
方式跳转
4. 权限处理
由主应用去控制,当用户登录成功后,返回动态路由表,并存入 store 中,然后将 store 通过 props 传给子应用,子应用根据 store 中的数据进行权限控制。
弱网环境下是懒加载好呢还是一次性加载好
在弱网环境下,优先推荐使用懒加载,并结合以下优化手段提升体验:
- 使用骨架屏提升首屏感知性能。
- 在空闲时段进行子应用预加载(如 prefetch)。
- 合理划分模块,控制每个懒加载 chunk 的体积。
- 使用 CDN 和 HTTP/2 加速静态资源加载。
qiankun
qiankun 的子应用只会根据
activeRule
匹配的 URL 渲染,只会加载一个子应用。并且子应用需要构建为UMD
格式。
优点
- 监听路由自动的加载、卸载当前路由对应的子应用
- 完备的沙箱方案
- js 沙箱做了三套渐进增强方案:
- 快照沙箱(
SnapshotSandbox
) - 支持单应用的代理沙箱(
LegacySandbox
) - 支持多应用的代理沙箱(
ProxySandbox
)
- 快照沙箱(
- css 沙箱做了两套:
- shadow DOM 方式(
strictStyleIsolation
) - css scope 方式(
experimentalStyleIsolation
)
- shadow DOM 方式(
- js 沙箱做了三套渐进增强方案:
- 路由保持,浏览器刷新、前进、后退,都可以作用到子应用
- 应用间通信简单,全局注入
缺点
- 基于路由匹配,无法同时激活多个子应用,也不支持子应用保活
- 改造成本较大,从 webpack、代码、路由等等都要做一系列的适配
- css 沙箱无法绝对的隔离,js 沙箱在某些场景下执行性能下降严重
- 无法支持 vite 等 ESM 脚本运行
样式污染怎么解决
可以不使用 qiankun 的样式隔离,使用 css-in-js 或者 css-modules 来解决。
原理
- qiankun 主要是监听路由变化,来加载对应的子应用。
md
- hash: window.onhashchange
- history: window.onpopstate
- history.go, history.back, history.forward 使用 popstate 事件监听
- pushState, replaceState 需要通过函数重写的方式进行劫持
- 子应用是通过
fetch
去请求子应用的entry
,然后通过import-html-entry
然后正则匹配得到内部样式表、外部样式表、内部脚本、外部脚本,组合之前为样式添加属性选择器(data-微应用名称);将组合好的样式通过 style 标签添加到 head 中。(外部样式表也会被读取到内容后写入<style>
中) - 创建 js 沙盒:不支持 Proxy 的用 SnapshotSandbox(通过遍历 window 对象进行 diff 操作来激活和还原全局环境),支持 Proxy 且只需要单例的用 LegcySandbox(通过代理来明确哪些对象被修改和新增以便于卸载时还原环境),支持 Proxy 且需要同时存在多个微应用的用 ProxySandbox(创建了一个 window 的拷贝对象,对这个拷贝对象进行代理,所有的修改都不会在 rawWindow 上进行而是在这个拷贝对象上),最后将这个 proxy 对象挂到 window 上面
- 执行脚本:将上下文环境绑定到 proxy 对象上,然后 eval 执行
注册采用的是 single-spa start 启动也是 single-spa 的方法,相比之下,qiankun 新增了下面的功能:
- 预加载功能:利用 requestIdleCallback 进行加载
- 沙箱功能:js 沙箱(创建 sandbox,让 execScript 方法运行在 sandbox 中) 样式隔离(影子 DOM,scoped css)
- 获取导出的接入协议(在沙箱中执行的),并进行扩展,然后放入 single-spa 的接入协议中
沙箱的作用
- JS 沙箱:防止子应用修改全局变量、污染全局 window 对象。
- CSS 沙箱(样式隔离):防止不同子应用之间的样式冲突或互相覆盖。
qiankun JS 沙箱 和 CSS 沙箱
JS 沙箱
- 快照沙箱(
SnapshotSandbox
)需要遍历 window 上的所有属性,性能较差。 - ES6 新增的 Proxy,产生了新的沙箱支持单应用的代理沙箱(
LegacySandbox
),它可以实现和快照沙箱一样的功能,但是性能更好,但是缺点是:也会和快照沙箱一样,污染全局的 window,因此它仅允许页面同时运行一个微应用。 - 而多应用的代理沙箱(
ProxySandbox
),可以允许页面同时运行多个微应用,并且不会污染全局的 window。
qiankun 默认使用 ProxySandbox
,可以通过 start.singular
来修改,如果发现浏览器不支持 Proxy
时,会自动优雅降级使用 SnapshotSandbox
。
SnapshotSandbox 快照沙箱
快照沙箱原理:A 应用启动前先保留 Window 属性,应用卸载掉的时候,把 A 修改的属性保存下来。等会新应用 B 进来我就把之前保留的原始未修改的 Window 给 B,应用 A 回来我就把 A 之前修改的属性还原回来。
这种方法比较浪费性能:因为需要给 Window 拍照,后续需要和 Window 进行对比。
简单实现:
js
class SnapshotSandbox {
constructor() {
this.windowSnapshot = {}
this.modifiedPropsMap = {}
}
active() {
// 当激活的时候,给 window 拍个快照
this.windowSnapshot = {}
Object.keys(window).forEach((key) => {
this.windowSnapshot[key] = window[key]
})
// 如果是重新调用 active 的时候,表示又一次激活,需要将保存的属性还原回来
Object.keys(this.modifiedPropsMap).forEach((key) => {
window[key] = this.modifiedPropsMap[key]
})
}
inactive() {
// 需要记录全局哪些属性被修改了
this.modifiedPropsMap = {}
// 如果失活,需要和快照的 window 进行对比,将变化的内容存入modifiedPropsMap
Object.keys(window).forEach((key) => {
if (window[key] !== this.windowSnapshot[key]) {
// 记录修改的属性
this.modifiedPropsMap[key] = window[key]
// 将 window 属性还原成最初样子
window[key] = this.windowSnapshot[key]
}
})
}
}
const sandbox = new SnapshotSandbox()
sandbox.active()
window.a = 100
window.b = 200
sandbox.inactive()
console.log(window.a, window.b) // undefined undefinded
sandbox.active()
console.log(window.a, window.b) // 100 200
LegacySandbox 单应用的代理沙箱
LegacySandbox
原理:只存储修改或添加的属性,不用给 window 拍照。需要存储新增的属性(addPropsMap
),修改前的属性(modifiedPropsMap
)和记录所有修改后、新增的属性(currentPropsMap
)。后续的操作是通过 Proxy
代理空对象进行处理的,例如:sandbox.proxy.a
是读取的 window 上的属性,sandbox.proxy.a = 100
是需要记录修改或新增的值,并且修改 window 上的属性。
这种方法的优点:性能比快照沙箱好(不用去监听整个 window)。缺点:Proxy
的兼容性不好,如果两个应用同时运行,window 只有一个,可能会有冲突。
modifiedPropsMap
,addPropsMap
是对失活的时候,进行属性还原的
currentPropsMap
是用在激活时候,进行属性还原的
js
class LegacySandbox {
constructor() {
/**
* modifiedPropsMap,addPropsMap是对失活的时候,进行属性还原的
*/
// 修改后,需要记录window上该属性的原值
this.modifiedPropsMap = {}
// 需要记录新增的内容
this.addPropsMap = {}
/**
* currentPropsMap是用在激活时候,进行属性还原的
*/
// 需要记录所有(不管修改还是新增),对于window上属性的删除就是修改
this.currentPropsMap = {}
// 创建了一个假 Window 对象,fakeWindow => {}
const fakeWindow = Object.create(null)
const proxy = new Proxy(fakeWindow, {
get: (target, key, receiver) => {
// 当取值的时候,直接从 window 上取值
return window[key]
},
set: (target, key, value, receiver) => {
if (!window.hasOwnProperty(key)) {
// 如果 window 上没有这个属性,则记录新增的属性
this.addPropsMap[key] = value
} else if (!this.modifiedPropsMap.hasOwnProperty(key)) {
// 如果 window 上有这个属性,并且之前没有修改过,需要记录原 window 上的值
this.modifiedPropsMap[key] = window[key]
}
// 无论新增还是修改,都记录一份(变化后的值)
this.currentPropsMap[key] = value
// 修改window上的属性,修改成最新的内容
window[key] = value
}
})
this.proxy = proxy
}
active() {
// 激活时,恢复之前的内容
Object.keys(this.currentPropsMap).forEach((key) => {
this.setWindowProps(key, this.currentPropsMap[key])
})
}
inactive() {
// 失活的时候,需要把修改的属性还原回去
Object.keys(this.modifiedPropsMap).forEach((key) => {
this.setWindowProps(key, this.modifiedPropsMap[key])
})
// 如果新增了属性,在失活时需要移除
Object.keys(this.addPropsMap).forEach((key) => {
this.setWindowProps(key, undefined)
})
}
// 设置window上的属性
setWindowProps(key, value) {
if (value === undefined) {
// 移除后面新增的属性
delete window[key]
} else {
// 变回原来的初始值
window[key] = value
}
}
}
const sandbox = new LegacySandbox()
// 需要去修改代理对象,代理对象setter后会去修改window上的属性
sandbox.proxy.a = 100
console.log(window.a, sandbox.proxy.a) // 100 100
sandbox.inactive()
console.log(window.a, sandbox.proxy.a) // undefined undefined
sandbox.active()
console.log(window.a, sandbox.proxy.a) // 100 100
ProxySandbox 多应用的代理沙箱
ProxySandbox
原理:产生各自的代理对象,读取的时候从代理对象上取值,没有再去读 window。修改的时候,设置代理对象,只需要修改代理对象即可,不会去操作 window。
优点:不会污染全局的 window。
js
class ProxySandbox {
constructor() {
// 控制激活和失活
this.running = false
const fakeWindow = Object.create(null)
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => {
// 获取的时候,先从 fakeWindow 上取值,如果取不到,再从 window 上取值
return key in target ? target[key] : window[key]
},
set: (target, key, value) => {
if (this.running) {
// 激活才去设置
target[key] = value
}
return true
}
})
}
active() {
if (!this.running) this.running = true
}
inactive() {
this.running = false
}
}
const sandbox1 = new ProxySandbox()
const sandbox2 = new ProxySandbox()
sandbox1.active()
sandbox2.active()
// 修改不会影响window,不用去还原window
sandbox1.proxy.a = 100
sandbox2.proxy.a = 200
console.log(sandbox1.proxy.a, sandbox2.proxy.a) // 100 200
sandbox1.inactive()
sandbox2.inactive()
sandbox1.proxy.a = 200
sandbox2.proxy.a = 400
console.log(sandbox1.proxy.a, sandbox2.proxy.a) // 100 200
子应用在使用时:把 sandbox1.proxy
当作 window
参数传递给子应用,子应用调用 window 上的属性时,会去 sandbox1.proxy
上取值。(LegacySandbox
同理)
js
;(function (window) {
console.log(window.a) // 100
})(sandbox1.proxy)
CSS 沙箱
qiankun 中提供了两种方案,一种是放入 Shadow DOM
中,具有完全的样式隔离;另一种是 CSS Scope
样式隔离,通过在容器节点添加 data-qiankun="子应用名"
属性,实现样式隔离。
html
<style>
div[data-qiankun='子应用名'] #app {
/* 子应用样式 */
}
</style>
除了 qiankun,还有哪些方案以及优劣?
🏆 single-spa
qiankun 和 single-spa 属于是“路由分发+资源处理”,就是通过监听路由变化,然后加载对应子应用资源。
采用 SystemJS
进行资源加载
缺点:
- 学习成本高(systemjs)
- 无沙箱机制,需要自己实现 js 沙箱和 css 隔离
- 需要对原有的应用进行改造
- 子应用间相同资源重复加载的问题
🏆 原生 iframe
优点:
- 使用简单
- 完全隔离(天然沙箱和样式隔离)
- 兼容性良好,组合灵活
缺点:
- dom 割裂严重的问题(例如:弹窗只能在 iframe 内部展示,无法覆盖全局)
- 子应用间通信麻烦
- 额外的性能开销,白屏时间长
- 路由状态丢失:刷新一下,iframe 的 url 状态就丢失了
🏆 无界(iframe + ShadowRoot)
无界微前端是一款基于 基于 WebComponent 容器(ShadowRoot) + iframe 沙箱的微前端框架。
原理:
无界采用 webcomponent 来实现页面的样式隔离,无界会创建一个 wujie 自定义元素,然后将子应用的完整结构渲染在内部。子应用的实例 instance 在 iframe 内运行,dom 在主应用容器下的 webcomponent 内,通过代理 iframe 的 document 到 webcomponent ,可以实现两者的互联。(也是通过和 qiankun 类似的 import-html-entry 进行分离)
无界对 iframe 的 window,document,location 进行了代理
优点:
- 解决了 iframe dom 割裂严重的问题
- 解决了路由状态丢失的问题,浏览器的前进后退可以天然的作用到 iframe 上,此时监听 iframe 的路由变化并同步到主应用,如果刷新浏览器,就可以从 url 读回保存的路由
- 解决了通信非常困难的问题,iframe 和主应用是同域的,天然的共享内存通信
上面 3 点,解决了 iframe 的缺点
- 可以同时激活多个应用
缺点:
- 内存占用较高,为了降低子应用的白屏时间,将未激活子应用的 shadowRoot 和 iframe 常驻内存并且保活模式下每张页面都需要独占一个 wujie 实例,内存开销较大。
- 兼容性一般,目前用到了浏览器的 shadowRoot 和 proxy 能力
- iframe 劫持 document 到 shadowRoot 时,某些第三方库可能无法兼容导致穿透。
🏆 Micro App
MicroApp 是京东开源的一款微前端框架,通过 web components + js 沙箱实现(Proxy 沙箱)。
优点:接入成本低
缺点:兼容性不好,没有降级方案
🏆 Module Federation
Module Federation(以下简称 MF) 是 Webpack5 的新特性,借助于 MF, 我们可以在多个独立构建的应用之间,动态地调用彼此的模块。
和上面的几种方案都不同,这种方案主要是远程模块资源共享。
优点:去中心化:没有“基座”概念,任何一个容器可以是远程(remote)也可以是主机(host)。
缺点:没有沙箱机制,无法做到隔离。