1. 谈谈你对微前端的理解,它解决了什么问题?
- 定义:一种类似于微服务的架构,将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。
- 为什么需要微前端?解决了什么问题?:
- 技术栈无关:主框架不限制接入应用的技术栈,子应用可自主选择。
- 独立开发、独立部署:各团队之间可以独立开发、测试、部署自己的应用。
- 增量升级:当需要对老旧系统进行升级时,可以用微前端的方式,用新的技术栈逐步替换。
- 团队自治:各团队可以更专注于自己的业务领域。
2. 实现微前端有哪些主流方案?它们各有什么优缺点?
实现微前端的主流方案,本质上是在解决两个核心问题:1. 应用加载 和 2. 应用隔离。主要有以下几种:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 1. iframe | 隔离性最强:天然的 JS、CSS 沙箱,几乎没有样式和全局变量污染。实现简单。 | 体验糟糕:URL 不同步,浏览器前进后退键失灵;弹窗和遮罩层只能在 iframe 内部,无法覆盖全局;通信复杂,只能用 postMessage,性能开销大。现在基本被淘汰。 |
| 2. Web Components | W3C 标准:无框架依赖,任何框架都能用。通过 Shadow DOM 实现样式和脚本的隔离。 | 兼容性问题:需要 Polyfills 兼容旧浏览器。改造有成本:需要将现有应用改造成 Web Component 规范。 |
| 3. single-spa (路由分发式) | 优秀的生命周期管理:它是一个元框架,能统一管理所有子应用的 bootstrap, mount, unmount 生命周期。技术栈无关:支持任何前端框架。 | 不够灵活:它只是个“调度员”,本身不解决 JS 和 CSS 的隔离问题,需要自己配合其他方案(如 CSS Modules, BEM)来解决。依赖加载方案:需要配合 SystemJS 或 Module Federation 等来加载远程模块。 |
| 4. Module Federation (模块联邦) | 真正的运行时共享:Webpack 5 的革命性功能。它允许一个应用在运行时动态加载另一个独立部署应用的模块,并且可以智能地共享依赖(如 React, Vue),避免重复加载,性能好。去中心化:每个应用都可以是“主应用”,也可以是“子应用”,非常灵活。 | 生态绑定:目前主要和 Webpack 生态强绑定,虽然其他构建工具也在跟进,但成熟度还不够。配置稍复杂:上手需要一定的学习成本。 |
总结对比:
iframe是“物理隔离”,简单粗暴但体验差。single-spa是“路由总管”,负责调度,但不负责装载和隔离。Module Federation是“模块快递员”,负责把远程模块安全、高效地送过来,并解决共享问题。
在现代项目中,“single-spa + Module Federation” 或者直接使用 Module Federation 是最推荐、最主流的组合拳。
3. 微前端架构下,你是如何解决以下问题的?
a) 应用间通信: 通信方案的选择取决于应用间的耦合关系,我通常会组合使用:
- URL/路由传递:对于不敏感、简单的数据,通过 URL 的
path或query参数传递,简单直观,能被用户分享。 - 自定义事件 (CustomEvent):基于
window对象派发和监听事件。这是一种非常解耦的方式,一个应用派发事件,其他应用监听即可。dispatchEvent和addEventListener。 - 全局状态管理:如果多个应用需要共享复杂的状态,可以把
Redux Store或Pinia Store实例暴露到全局window上,或者由主应用创建后通过 props 注入。缺点是会增加耦合度。 - props 传递:主应用可以直接通过 props 向子应用传递数据和回调函数,这是最简单的方式,适用于父子关系明确的场景。
b) 样式隔离: 样式污染是微前端的头号敌人,必须严格处理:
- CSS-in-JS:如
styled-components,天生就能生成唯一的类名,作用域被限定在组件内,不会泄露。 - Scoped CSS:主要在 Vue 中使用,核心原理是通过给 DOM 元素和 CSS 规则添加唯一的自定义属性来限定作用域,比如,一个 .title 的类,在 scoped 模式下会变成 .title[data-v-12345],它的优点是使用非常简单。
- CSS Modules:在 React 中更常见,核心原理是在编译时将 CSS 类名本身程序化、哈希化,使其在全局变得独一无二。比如 .title 会变成一个唯一的哈希,像 .Component_title__aBcDeF。你必须在 JS 中 import 样式文件,然后像使用 JS 对象(styles.title)一样来使用类名。它的优点是隔离性最强,是完全的;缺点是写法上比原生 CSS 繁琐。
- Shadow DOM:Web Components 的原生隔离方案,将样式完全封装在内部。
- BEM 命名规范 或 为 CSS 选择器加前缀:这是一种约定式的弱隔离方案,依赖开发者的自觉性。比如,用户中心的 CSS 都以
.user-center-开头,但有维护成本和冲突风险。
c) 公共依赖: 避免每个子应用都打包一份 React、Vue、Ant Design,导致页面加载性能雪崩:
- Webpack Externals:在构建时不打包某些库,而是假设它们由外部提供(如通过
<script>标签在index.html中引入)。这是一种传统但有效的方案。 - Import Maps:一个新兴的浏览器标准,它允许你控制浏览器在解析
import语句时如何查找模块。你可以指定import 'react'时,去加载 CDN 上的某个特定版本的 React。single-spa经常配合import-map-overrides来实现。 - Module Federation 的
shared配置:这是最完美的方案。你可以在shared数组里声明需要共享的依赖(如react),Webpack 会在运行时智能判断:如果主应用已经加载了 React 18,其他子应用就不会再加载,而是直接复用主应用的实例。
4. 如果让你从零开始设计一个微前端架构,你的技术选型和设计思路是什么?
我的设计会遵循以下步骤:
- 确定主应用(或称基座应用):我会选择一个轻量级的技术栈(比如 React 或 Vue)来搭建主应用。它的职责非常单一:只负责应用注册、路由分发、全局状态管理和公共资源加载,不包含任何业务逻辑。
- 技术选型定案:
- 框架选型:我会选择
single-spa作为上层路由和生命周期管理的“总调度室”。 - 模块加载:我会选择
Webpack Module Federation作为底层子应用的加载和依赖共享方案。这个组合兼具了single-spa清晰的生命周期管理和Module Federation高效的运行时共享能力。
- 框架选型:我会选择
- 制定规范和协议:
- 通信协议:定义清晰的全局事件名或共享状态的读写规范。
- 样式方案:强制所有子应用使用 CSS Modules 或 CSS-in-JS 来避免样式冲突。
- 公共组件库:建立一个独立的、由所有团队共同维护的共享组件库,发布为 npm 包,确保所有子应用的 UI/UX 体验一致。
- 设计部署流程:
- 每个子应用都有自己独立的 Git 仓库和 CI/CD 流水线,可以随时独立部署上线,不依赖其他应用。
- 主应用也独立部署。子应用的部署只更新自己的 JS 文件,主应用通过路由配置动态拉取即可,无需重新部署主应用。
- 设计沙箱和隔离:
- JS 沙箱:为了防止子应用修改全局
window对象或document事件互相污染,我会引入一个简易的 JS 沙箱。在子应用挂载时,通过 Proxy 劫持window,使得对全局对象的操作被隔离在沙箱内部;卸载时恢复。像qiankun框架就内置了完善的沙箱机制。 - CSS 沙箱:除了之前提到的样式隔离方案,也可以在子应用加载时,动态地为所有样式规则加上特定的属性选择器(如
div[data-app-name="app1"] p { ... }),并在卸载时移除,实现更严格的隔离。
- JS 沙箱:为了防止子应用修改全局
5. qiankun 的核心原理是什么?(从加载、沙箱、隔离三个方面回答)
-
应用加载机制:基于
import-html-entry这个库。当匹配到子应用路由时,qiankun会通过fetch获取子应用的index.html文件。然后,它会解析这个 HTML 文件,提取出其中的 JS 和 CSS 资源链接。最后,它会加载这些资源,并按顺序执行 JS 脚本,从而完成子应用的渲染。 -
JS 沙箱:为了防止不同子应用间的全局变量冲突,
qiankun实现了 JS 沙箱。它主要通过Proxy来创建一个代理的window对象(fakeWindow)。当子应用访问或修改全局变量时(如window.a = 1),所有操作都会被拦截并作用在这个fakeWindow上,而不会影响到真实的window对象,从而实现了完美的隔离。对于不支持Proxy的旧版浏览器,它会降级为使用快照沙箱。 -
CSS 隔离:为了避免全局样式污染,
qiankun提供了两种方案。最常用的一种是实验性样式隔离,它会在子应用加载时,通过 PostCSS 等工具遍历其所有 CSS 规则,并为每个选择器自动添加一个特殊的属性前缀,如div[data-qiankun="app-name"] .title { ... },同时为主应用中承载子应用的容器节点也加上这个属性,从而确保样式只在子应用内部生效。另一种是基于 Shadow DOM 的严格隔离方案,但因其完全隔离的特性,可能会导致一些弹窗类组件样式失效,使用较少(两个方案都存在弹窗样式问题)。
6. qiankun 是如何实现 JS 沙箱的?
qiankun 实现 JS 沙箱的核心是隔离不同子应用之间的全局变量,避免互相污染。
-
SnapshotSandbox(快照沙箱):- 原理:在子应用挂载 (
mount) 前,它会遍历当前的window对象,用一个Map记录下所有属性的快照。在子应用卸载 (unmount) 时,它会再次遍历window对象,与之前的快照进行对比,将所有被修改或新增的属性恢复到原始状态。 - 区别/缺点:这种方式决定了它无法同时支持多个子应用实例共存。因为只有一个全局
window,恢复快照会影响到所有应用,所以同一时间只能有一个子应用处于激活状态。它主要用于兼容不支持Proxy的老旧浏览器。
- 原理:在子应用挂载 (
-
LegacySandbox(单例代理沙箱):- 原理:
SnapshotSandbox的升级版。在支持Proxy的浏览器中,但只运行单个子应用实例的场景下启用。它也使用Proxy拦截,但所有子应用实例共享同一个Proxy沙箱,失活时需要遍历还原window上的属性。 - 缺点:同样只支持单实例运行。是
ProxySandbox之前的一个过渡形态。
- 原理:
-
ProxySandbox(代理沙箱):- 原理:利用 ES6 的
Proxy特性,为每个子应用创建一个“伪造”的window对象(fakeWindow)。当子应用内部执行window.a = 1或读取window.a时,Proxy会捕获这些操作,并将它们代理到fakeWindow对象上。子应用读取属性时,会先从fakeWindow找,找不到再从真实的window上找。 - 区别/优点:由于每个子应用都有自己独立的
fakeWindow,它们之间的全局变量是完全隔离的,因此可以完美支持多个子应用同时运行且互不干扰。这是在现代浏览器中的默认和推荐方案。
- 原理:利用 ES6 的
7. qiankun 是如何实现样式隔离的?有哪几种方式,优缺点是什么?
qiankun 提供了两种 CSS 隔离方案:
-
strictStyleIsolation(严格样式隔离)- 方式:通过
Shadow DOM实现。qiankun会将子应用完全渲染在一个Shadow DOM内部,Shadow DOM提供的原生能力可以确保内部的样式和外部的样式、内部的 DOM 和外部的 DOM 完全隔离。 - 优点:提供了最严格的隔离,原生实现,无需任何 CSS 预处理。
- 缺点:一些依赖于挂载到
document.body的 UI 组件(如 Modal、Drawer)会因为无法穿透Shadow DOM的样式边界而导致样式丢失。
- 方式:通过
-
experimentalStyleIsolation(实验性样式隔离)- 方式:通过动态修改 CSS 选择器范围实现。它会为子应用的所有 CSS 规则自动添加一个特殊的属性选择器作为前缀,例如
[data-qiankun="app-name"]。 - 优点:兼容性好,没有
Shadow DOM的弹窗样式问题,是目前最常用和推荐的方案。 - 缺点:隔离性不如
Shadow DOM严格,如果主应用中有一些非常通用的标签选择器(如body, html)且权重很高,理论上仍有污染的可能性
- 方式:通过动态修改 CSS 选择器范围实现。它会为子应用的所有 CSS 规则自动添加一个特殊的属性选择器作为前缀,例如
8. 主应用和子应用之间是如何通信的?GlobalState 的原理是什么?
主子应用通信主要有两种方式:
-
基于
props的单向通信:在主应用注册子应用时,可以通过props属性将数据或函数传递给子应用。子应用在mount生命周期中可以接收到这些props。这种方式简单直接,但通常用于主应用向子应用单向传递信息。 -
基于
initGlobalState的双向通信:这是qiankun官方推荐的方式。- 原理:它的底层实现是一个经典的发布-订阅模式。
initGlobalState(initialState):在主应用中调用,它会创建一个全局的状态存储对象,并返回onGlobalStateChange和setGlobalState两个方法。setGlobalState(state):相当于发布者。无论是主应用还是子应用调用它,都会更新全局状态,并通知所有订阅者。onGlobalStateChange((state, prev) => ...):相当于订阅者。主应用和子应用都可以通过它来注册一个回调函数,监听全局状态的变化,一旦状态被setGlobalState修改,所有注册的回调函数就会被执行。
- 原理:它的底层实现是一个经典的发布-订阅模式。
9. 你在使用 qiankun 的过程中如何项目改造?遇到过哪些坑?是怎么解决的?
-
项目改造
- 主应用:
- 安装
qiankun。 - 使用
registerMicroApps(apps, lifeCycles)注册子应用列表。 - 使用
start(opts)启动qiankun。 - 设置容器 DOM。
- 安装
- 子应用:
- 在入口文件 (
main.js/main.ts) 中导出bootstrap,mount,unmount三个生命周期函数。 - 修改
webpack打包配置:output.library: 应用名,需与主应用注册时一致。output.libraryTarget:umd。output.jsonpFunction:webpackJsonp_${packageName}。
- 配置路由的
base属性,以匹配主应用中激活的规则。
- 在入口文件 (
- 主应用:
-
常见问题 (踩坑指南)
- 静态资源加载失败 (404):
- 问题:图片、字体等资源相对路径错误。
- 解决:利用
webpack的publicPath动态设置资源前缀。qiankun会向子应用注入window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__。
- 路由问题:
- 问题:子应用路由无法正常工作或跳转到了主应用。
- 解决:为子应用的
react-router或vue-router设置正确的basename。
- 跨域问题:
- 问题:开发时主应用
fetch子应用资源时出现跨域。 - 解决:子应用
webpack-dev-server配置headers: { 'Access-Control-Allow-Origin': '*' }。
- 问题:开发时主应用
Keep-Alive状态保持:qiankun自身不直接支持,但可以结合React.lazy/Vue的keep-alive和qiankun的loadMicroApp手动管理子应用实例,或者利用umi-plugin-qiankun等社区方案。
- 弹窗挂载问题:
- 问题:
Ant Design的Modal等组件默认挂载到document.body,在Shadow DOM隔离模式下样式会丢失。 - 解决:通过组件库提供的
getContainerAPI,将弹窗挂载到子应用内部的 DOM 节点上。
- 问题:
- 静态资源加载失败 (404):
10. qiankun 和 Webpack Module Federation 的区别是什么,你会怎么选型?
-
核心区别:
qiankun是“应用级”的运行时方案:它将每个子系统视为一个完整的应用,在运行时通过路由来加载和管理这些应用。它的核心是应用隔离和生命周期管理。Module Federation是“模块级”的编译时方案:它允许一个 Webpack 构建产物在运行时去动态加载另一个构建产物暴露出来的模块(如组件、函数)。它的核心是模块共享。
-
如何选型:
- 当你的目标是整合多个独立、完整、异构的应用时,特别是处理包含不同技术栈的历史项目时,
qiankun是更好的选择。它提供了开箱即用的沙箱和隔离,让应用整合变得简单。 - 当你的目标是在多个应用之间共享组件库、工具函数等代码片段,而不是整个应用时,
Module Federation更合适。它避免了重复打包和加载公共依赖的问题,粒度更细。 - 两者也可以结合使用:例如,使用
qiankun作为顶层的应用治理框架来组织不同的业务应用,而在这些应用之间,可以使用Module Federation来共享通用的组件库,实现优势互补。
- 当你的目标是整合多个独立、完整、异构的应用时,特别是处理包含不同技术栈的历史项目时,
11. Wujie(无界)框架的核心原理
1. JavaScript 沙箱:iframe 幽灵 + Proxy 代理
这是 Wujie 最巧妙、最核心的部分。传统的 JS 沙箱要么用 Proxy 硬扛(有兼容性问题和性能损耗),要么用 with + eval(有安全风险和性能问题)。Wujie 另辟蹊径:
-
创建“幽灵”
iframe:- 当加载一个子应用时,Wujie 会在背后创建一个不可见的、存在于内存中的
iframe。这个iframe并不用来显示任何东西。 - 它的唯一目的,是提供一个纯净、隔离的
window对象。我们称之为shadowWindow。这个window与主应用的window是完全隔离的,拥有自己独立的document,location,history等一整套全局变量。
- 当加载一个子应用时,Wujie 会在背后创建一个不可见的、存在于内存中的
-
建立
Proxy代理:- Wujie 将子应用的所有代码都运行在这个
shadowWindow的环境中。 - 同时,它使用
Proxy拦截了子应用对shadowWindow的所有操作(比如window.location = '...'或者document.createElement(...))。
- Wujie 将子应用的所有代码都运行在这个
-
重定向操作:
- 对于变量读写:当子应用读写全局变量(如
window.a = 1)时,Proxy会确保这些操作只发生在隔离的shadowWindow上,绝对不会污染主应用的window。 - 对于 DOM 操作:当子应用调用
document.createElement或document.body.appendChild等 DOM API 时,Proxy会拦截这些操作,然后将它们转发到主应用的document上去执行。
- 对于变量读写:当子应用读写全局变量(如
一句话总结:子应用的 JS 以为自己活在一个独立的 iframe 世界里(变量隔离),但它创造的“肉身”(DOM 元素)却被 Wujie 悄悄地搬运到了主应用的世界里(UI 呈现)。
这种方式既利用了 iframe 的原生隔离性,又避免了 iframe 的所有 UI 和体验问题。
2. CSS 隔离:Web Components + Shadow DOM
Wujie 的 CSS 隔离方案则完全拥抱了 Web 标准:
- 创建自定义元素:Wujie 会将承载子应用的容器 DOM 节点,通过
customElements.define定义成一个 Web Component。 - 启用
Shadow DOM:然后为这个 Web Component 开启Shadow DOM(影子 DOM)。 - 样式注入:子应用的所有
style标签和link标签都会被移动到这个Shadow DOM内部。
效果:Shadow DOM 就像一个结界,它内部的样式绝对不会泄露出去影响主应用或其他子应用;同时,外部的样式也绝对不会渗透进来影响子应用。这是浏览器原生提供的、最可靠的样式隔离方案。
3. 路由同步:劫持 + 同步
为了解决 iframe 中 URL 不同步、浏览器前进后退键失灵的问题,Wujie 做了:
- 劫持子应用路由:在 JS 沙箱内部,Wujie 劫持了
shadowWindow的history.pushState和history.replaceState等方法。 - 同步到主应用:当子应用调用这些方法尝试改变 URL 时,Wujie 会捕获这个行为,然后调用主应用的路由实例去改变浏览器地址栏的真实 URL(通常是加上特定的前缀或参数来区分)。
这样,无论子应用内部如何跳转,最终都会反映在浏览器顶层的地址栏上,用户体验就像一个单体应用一样流畅。
12. micro-app 框架的核心原理
1. 组件化封装:一切皆为 Custom Element
这是 micro-app 架构的基石。它将整个子应用看作是一个“组件”。
- 定义自定义元素:
micro-app通过window.customElements.define定义了一个名为<micro-app>的 HTML 标签。 - 实例化子应用:当你在主应用中使用
<micro-app name='xx' url='...'></micro-app>时,实际上就是在实例化这个自定义元素。micro-app会监听这个元素的生命周期(如connectedCallback、disconnectedCallback),在合适的时机去加载、渲染和卸载子应用。 - 优点:
- 极简接入:使用方式和普通的
div、span几乎没有区别,非常符合前端开发者的直觉。 - 技术栈无关:因为是 W3C 标准,所以无论主应用是 React、Vue 还是 Angular,都可以轻松使用。
- 极简接入:使用方式和普通的
2. JavaScript 沙箱:with + Proxy 的巧妙结合
为了隔离 JS 环境,防止全局变量污染,micro-app 实现了一套非常高效的沙箱机制。
- 创建代理
window:当加载子应用时,micro-app不会使用iframe,而是用Proxy创建一个全新的、纯净的window代理对象(我们称之为proxyWindow)。 with绑定执行上下文:micro-app会将子应用的所有 JS 代码包裹在一个with(proxyWindow) { ... }的代码块中执行。- 变量的读写规则:
- 写入操作:当子应用执行
window.a = 1时,由于with的作用,这个a属性会被直接设置到proxyWindow上,而不会影响到真实的window。 - 读取操作:当子应用读取
window.a时,它会首先在proxyWindow上寻找。如果找不到,Proxy的机制会让它向上穿透,去真实的window对象上寻找。
- 写入操作:当子应用执行
一句话总结:写操作只在自己的沙箱内生效,读操作可以读取沙箱和主应用的值。
这种设计非常精妙,它既保证了子应用之间的隔离,又允许子应用能够获取到主应用环境提供的一些全局能力(比如 document、location 等),避免了像 Wujie 那样需要手动转发大量 window 属性的复杂性。
3. CSS 隔离:Scoped CSS 的自动化
micro-app 并没有使用 Shadow DOM 来做样式隔离,而是实现了一套类似 Vue 中 scoped 样式的方案。
- 加载和转换:当加载子应用的 CSS 文件时,
micro-app会在运行时(in-memory)解析 CSS 规则。 - 添加动态属性:它会为子应用的所有 DOM 元素动态添加一个唯一的自定义属性,例如
<div data-micro-app="app1"></div>。 - 重写 CSS 选择器:同时,它会重写所有的 CSS 规则,在每个选择器后面都加上这个属性选择器。例如,
div { color: red; }会被改写成div[data-micro-app="app1"] { color: red; }。
效果:这样一来,子应用的所有样式都只会精确地应用在带有特定属性的 DOM 元素上,从而实现了严格的样式隔离,避免了对全局环境的污染。