跳到主要内容
面试知识DEV CATALOGNOTE

微前端面试知识整理

微前端是一套架构思路,不绑定某个具体框架或构建工具——Webpack 5 的模块联邦(Module Federation,见《Webpack 面试知识整理》第十章)只是实现微前端的其中一种手段。本文覆盖微前端要解决的问题、主流落地方案(iframe、Web Components、single-spa、qiankun)、以及背后的应用隔离、通信、路由协同等通用机制。

复习建议

  • 先搞清楚微前端解决的是"多团队独立开发部署"这个组织问题,而不是单纯的技术炫技,回答动机题时从这个角度切入更站得住。
  • 隔离(JS 沙箱、CSS 隔离)是这个话题里源码/原理向的核心,建议能讲清楚 Proxy 快照沙箱的基本思路。
  • 区分"某个框架(如 qiankun)的具体 API"和"微前端的通用问题",避免把两者混着答。

一、微前端是什么:要解决的问题

大型前端项目发展到一定规模后,会遇到一类和"代码怎么写"无关、而是和"团队怎么协作"相关的问题:多个团队共同维护同一个巨大的单体前端应用,任何一个团队发布都可能影响到其他团队的功能;技术栈被迫统一,某个历史模块想升级框架版本,往往要拖着整个应用一起升级;应用体量越滚越大,构建和部署的耦合成本也越来越高。

面试回答:

微前端是把一个大型前端应用拆分成多个可以独立开发、独立测试、独立部署的子应用,再通过一个主应用(或者说容器应用)在运行时把它们组合成一个完整产品的架构方式。它借鉴了后端微服务的思路,核心目标是让多个团队能够并行推进、独立发布,而不需要跟着同一个发布节奏、被同一套技术栈捆绑。

需要澄清一个常见误区:微前端不是"把一个页面拆成几个 iframe"这么简单,也不是所有项目都应该上微前端。它主要解决的是多团队协作层面的问题;如果项目本身就是一个团队维护、体量也不大,引入微前端带来的额外复杂度(应用间通信、样式隔离、公共依赖管理)往往得不偿失。

二、主流实现路线对比

方案核心思路优点代价
iframe每个子应用运行在独立的 iframe 里天然的 JS/CSS 隔离,实现成本最低应用间通信要靠 postMessage,路由/前进后退体验割裂,SEO 和首屏性能较差,弹层/下拉这类需要跨越 iframe 边界的 UI 很难做
Web Components子应用封装成自定义元素,利用 Shadow DOM 做样式隔离浏览器原生标准,天然样式隔离JS 全局作用域依然共享,需要额外方案处理;框架间桥接有一定成本
single-spa提供一套应用注册、生命周期调度的框架无关内核灵活、不绑定实现细节,社区生态成熟隔离能力等基础设施需要自己搭建或依赖插件
qiankun基于 single-spa 二次封装,内置沙箱隔离和资源加载开箱即用,接入成本低,国内生态和文档完善相比 single-spa 定制自由度略低;基于 iframe/Proxy 沙箱的方案在极端场景仍有兼容性坑

面试回答:

这几种方案的核心差异在于"隔离粒度"和"接入成本"的取舍。iframe 隔离最彻底但体验代价最大;Web Components 只解决了样式隔离,JS 隔离仍要另外处理;single-spa 提供的是一套应用调度的骨架,隔离能力要自己补;qiankun 站在 single-spa 之上,把沙箱和资源加载这些基础设施都封装好了,是目前接入成本最低的路线之一。选型时要结合团队现有技术栈、对隔离强度的要求和迁移成本一起权衡,没有绝对的最优解。

三、应用生命周期管理:注册、加载、挂载、卸载

不管具体用哪个框架,微前端框架内核基本都要解决同一个问题:主应用如何知道该在什么时机加载、挂载、卸载哪个子应用。以 single-spa/qiankun 的模型为例,一个子应用需要导出四个标准生命周期函数:

// 子应用入口,导出标准生命周期
export async function bootstrap() {
// 只在应用首次加载时执行一次,用于初始化,不涉及具体渲染
}

export async function mount(props) {
// 每次应用被激活(路由匹配上)时执行,负责把应用渲染到指定容器
ReactDOM.render(<App />, props.container.querySelector('#root'))
}

export async function unmount(props) {
// 应用被切走时执行,必须清理定时器、事件监听、渲染结果等
ReactDOM.unmountComponentAtNode(props.container.querySelector('#root'))
}

主应用侧则注册每个子应用对应的路由规则和资源加载方式:

registerMicroApps([
{
name: 'react-app',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/react',
},
])
start()

主应用内部维护一个调度循环:监听路由变化 → 找出应该激活的子应用 → 如果这个子应用还没加载过,先拉取它的 HTML/JS/CSS 资源并执行 bootstrap → 调用 mount 挂载 → 用户离开这个路由时调用 unmount 卸载、清理沙箱。卸载阶段是最容易被面试追问、也最容易在真实项目里出问题的环节——如果 unmount 没有正确清理子应用注册的全局事件监听、定时器、WebSocket 连接,即使 DOM 被移除了,这些副作用依然会在后台运行,造成内存泄漏或者诡异的跨应用干扰(比如卸载的子应用里的定时器还在偷偷修改一个被两个应用共享的全局变量)。

四、JS 沙箱隔离

为什么需要沙箱

多个子应用如果直接在同一个 window 全局作用域下运行,会互相污染:A 应用往 window 上挂了一个变量,B 应用可能意外读到甚至覆盖它;A 应用切走时如果不清理,可能残留一些全局状态影响到之后加载的 B 应用。JS 沙箱要解决的核心问题是:让每个子应用"看起来"运行在一个独立的全局环境里,互不干扰,卸载时能干净地清理掉这次运行产生的全局副作用

快照沙箱(Snapshot Sandbox)

思路最简单直接:应用挂载前,把当前 window 上的所有属性做一份快照记录下来;应用运行期间对 window 做的所有修改照常生效;应用卸载时,对比当前 window 和快照的差异,把这次运行新增/修改的属性全部还原成挂载前的状态。

class SnapshotSandbox {
constructor() {
this.windowSnapshot = {};
this.modifyPropsMap = {};
}
active() {
// 挂载前:把当前 window 上的每个属性存一份快照
this.windowSnapshot = {};
for (const prop in window) {
this.windowSnapshot[prop] = window[prop];
}
// 恢复上一次卸载时记录下来的修改,让应用重新挂载时环境和上次卸载时一致
Object.keys(this.modifyPropsMap).forEach((prop) => {
window[prop] = this.modifyPropsMap[prop];
});
}
inactive() {
// 卸载前:找出这次运行期间被修改或新增的属性,记录下来,再把 window 还原成快照状态
this.modifyPropsMap = {};
for (const prop in window) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
}
}

这种方案实现简单,但同一时间只能有一个应用处于激活状态——因为它直接操作唯一的全局 window,两个应用同时挂载会互相冲盖快照。它适合"同一时间只展示一个子应用"这种主流场景,不适合需要多个子应用同时运行的场景。

基于 Proxy 的沙箱

为了支持多个应用同时运行(比如可能存在的并行沙箱场景),更现代的方案是给每个子应用分配一个"假的全局对象",用 Proxy 拦截这个假对象上的读写:

function createProxySandbox() {
const fakeWindow = {};
const proxy = new Proxy(fakeWindow, {
get(target, key) {
// 优先读取沙箱自己修改过的属性,否则透传到真实 window 上读取
return key in target ? target[key] : window[key];
},
set(target, key, value) {
// 所有写操作都只落在这个沙箱自己的 fakeWindow 上,不污染真实 window
target[key] = value;
return true;
},
has(target, key) {
return key in target || key in window;
},
});
return proxy;
}

每个子应用的代码在执行时,其内部访问的"全局对象"被替换成这个 proxy:读取时如果沙箱自己没有这个属性就去真实 window 上找(因此能正常读取到浏览器原生的全局对象,如 documentfetch),但所有的写操作都只会落在这个应用私有的 fakeWindow 上,不会真正修改到共享的 window。多个子应用各自持有自己的 fakeWindow,天然支持多应用同时运行、互不干扰;应用卸载时直接丢弃这个 fakeWindow 对象即可,不需要像快照方案那样费力对比差异。

代价是所有全局变量的读写都要经过一层 Proxy 拦截,有一定的性能开销(现代浏览器下通常可以接受);此外像 evalwith 语句里对全局作用域的隐式访问,以及一些框架对 window === globalThis 之类的严格相等判断,在代理场景下需要额外适配,不是所有代码都能完全无感知地运行在代理沙箱里。

五、CSS 隔离

多个子应用的 CSS 如果不做隔离,很容易出现类名冲突——A 应用定义了 .container 的样式,恰好 B 应用也用了同名类名,样式会互相覆盖。常见的隔离方案:

方案思路特点
Shadow DOM把子应用挂载到一个 Shadow Root 内浏览器原生支持,隔离最彻底;但部分依赖 document.querySelector 全局查找 DOM 的第三方库可能在 Shadow DOM 里表现异常
动态样式作用域(Scoped CSS)运行时给子应用的每条 CSS 规则自动加上对应的属性选择器前缀兼容性更好,不依赖浏览器新特性;实现和调试成本比 Shadow DOM 高
CSS Modules / CSS-in-JS构建时通过哈希类名从根本上避免命名冲突需要子应用自身在构建阶段就采用这类方案,属于开发规范层面的约束,而不是微前端框架运行时提供的能力
命名空间约定团队约定每个应用的所有类名都带上统一前缀成本最低,但完全依赖人为约束,规模大了容易失守

qiankun 默认支持 Shadow DOM 严格隔离模式和一种运行时动态生成属性选择器的 Scoped CSS 模式,二者可以按需选择——前者隔离最彻底但兼容性问题更容易暴露,后者兼容性更好但本质上是一种"尽力而为"的隔离,无法做到 Shadow DOM 那样浏览器层面的强制边界。

六、应用间通信与状态共享

主应用和子应用、子应用和子应用之间不可避免会需要共享一些状态(比如登录用户信息、全局主题)或者互相触发一些行为(比如子应用要通知主应用切换路由)。常见方式:

  • 主应用下发全局状态:主应用维护一份全局 store,通过 props 的方式在挂载子应用时传给子应用,子应用通过订阅/回调的方式读取变化。这是最常见、也最容易控制数据流向的方式。
  • 自定义事件总线:基于浏览器原生的 CustomEvent,或者一个简单的发布订阅实现,各应用间通过事件通信,不需要强绑定谁是主应用谁是子应用。
  • URL 参数 / 路由 state:把需要跨应用传递的少量信息编码进 URL,天然支持跨应用间通过导航传递,也方便刷新后保持状态。

不建议子应用之间通过直接读写 window 上的共享变量来通信——这和沙箱隔离的初衷是冲突的,容易在调试时很难定位到底是哪个应用在什么时机修改了这个变量,应该通过主应用统一维护的、有明确读写入口的状态层来中转。

七、路由分发与主子应用协同

主应用通常维护一份"路由前缀 → 子应用"的映射表(如第三章示例中的 activeRule),监听浏览器的路由变化(popstate/hashchange,或者拦截 pushState/replaceState),匹配到某个子应用的路由规则后触发对应的加载/挂载流程。

需要注意两层路由是独立的:主应用的路由框架负责"当前应该显示哪个子应用",子应用内部往往还有自己的一套路由(比如内部各个页面之间的跳转),这层路由要以主应用分配给它的前缀作为 base path,避免和其他子应用的路由规则冲突。子应用独立运行开发调试时和被主应用集成运行时,路由 base path 往往不同,需要做成可配置项,而不是硬编码。

八、静态资源加载与 HTML Entry

qiankun 等方案常用的资源接入方式是 HTML Entry:主应用直接请求子应用部署后的入口 HTML(而不是要求子应用单独暴露一个 manifest 文件),解析这份 HTML 里引用的 JS、CSS 资源列表,逐个请求下来,把 JS 用沙箱环境执行、CSS 按隔离策略插入。这样做的好处是子应用几乎不需要为了接入微前端框架而改造自己的构建产物,独立部署、独立访问和被主应用集成用的是同一份构建产物。

需要留意的实际问题:子应用的资源加载失败(部署地址不可用、跨域没配置好)会导致整个子应用无法挂载,通常需要配合超时和失败重试、以及友好的错误兜底 UI;子应用资源体积、加载耗时也会直接影响主应用切换到这个子应用时的体验,值得纳入性能监控。

九、部署、灰度与版本管理

微前端的一个重要收益是"独立部署",但独立部署也带来新的工程问题:主应用如何知道该加载子应用的哪个版本?常见做法是主应用维护的子应用 entry 地址指向一个稳定的路径(比如始终指向最新版本),子应用每次发布直接覆盖这个路径下的产物;需要灰度发布时,可以在这个 entry 解析层做流量分配(比如按用户 ID 哈希决定加载灰度版本还是稳定版本的资源地址)。

版本兼容也是容易被忽视的坑:如果子应用和主应用之间约定了某种通信协议或者共享了某个基础库,子应用独立发布时改动了这个约定,而主应用没有同步更新,会在运行时才暴露出不兼容问题——这和第十章 Module Federation 里提到的"版本耦合风险"是同一类问题,只是发生的层面不同。

十、与 Module Federation 的关系和选择

Module Federation(详见《Webpack 面试知识整理》第十章)本质上解决的是"运行时跨应用共享模块"这一个具体能力,而不是一整套微前端解决方案——它没有内置沙箱隔离、没有内置应用生命周期调度、也没有内置路由分发机制,这些都需要自己在此基础上搭建,或者结合 qiankun/single-spa 一起使用。

维度qiankun / single-spaModule Federation
定位完整的微前端应用调度框架,负责生命周期、隔离、资源加载Webpack 提供的运行时模块共享能力
隔离能力内置 JS 沙箱和 CSS 隔离方案不提供,需要自行处理或不追求严格隔离
适合场景多个技术栈不同、需要严格隔离的独立子应用整合成一个产品同一技术栈内、希望在运行时共享部分组件/逻辑,减少重复打包和重复发布
典型例子把用 Vue 写的老系统和用 React 写的新系统整合到一个门户多个使用同一套 React 技术栈的应用,共享一套业务组件库,且希望更新后不用所有应用重新构建

实践中两者不是非此即彼:可以在 qiankun 搭建的整体应用骨架下,某几个技术栈相同、耦合更紧密的子应用之间再用 Module Federation 做更细粒度的模块共享,兼顾整体架构的隔离性和局部场景下的共享效率。

十一、面试冲刺索引

⭐⭐⭐⭐⭐ 核心必会

  • 微前端解决的是多团队独立开发部署的问题,不是单纯的技术选型。
  • 应用生命周期四段式(bootstrap/mount/unmount)以及卸载阶段清理不彻底会导致的问题。
  • JS 沙箱:快照沙箱与 Proxy 沙箱的思路差异,能画出/讲出 Proxy 沙箱的核心实现。
  • CSS 隔离的几种方案与各自的兼容性代价。

⭐⭐⭐⭐ 高频重点

  • 主流实现路线(iframe/Web Components/single-spa/qiankun)的取舍对比。
  • 应用间通信为什么不建议直接共享全局变量。
  • HTML Entry 的资源加载方式,以及子应用加载失败该如何兜底。
  • Module Federation 和 qiankun/single-spa 各自解决的问题、可以如何配合使用。

容易连续追问的知识链

  • 单体应用协作痛点 → 微前端拆分 → 主应用调度子应用生命周期 → 卸载阶段清理副作用。
  • 多应用共享全局 window → 互相污染 → 快照沙箱 → 只能单应用激活的局限 → Proxy 沙箱支持多应用并存。
  • CSS 类名冲突 → Shadow DOM 强隔离 / Scoped CSS 弱隔离 → 第三方库兼容性风险。
  • 独立部署诉求 → entry 地址与灰度发布 → 子应用与主应用之间的协议版本耦合风险。