首页  |  学习总览  |  ← 返回专题总览 进阶专题 04 · 架构模式

微前端全面解析 🔥 重点

巨石应用的拆迁方案:qiankun / micro-app / wujie / Module Federation 四大方案 + JS 沙箱与样式隔离源码级拆解
学习时长:约 14 小时 | 前置:阶段 05 / 09 | 产出:基座 + 2 个子应用可运行工程

一、微前端到底解决什么问题?

巨石应用(Monolith SPA)的三大痛点

  1. 开发协作难:200 人改同一个仓库,冲突不断、发布互相阻塞,一个模块出问题全站挂。
  2. 技术栈锁定:老系统 AngularJS 想局部用 React 都难,重构要"推倒重来"。
  3. 体验割裂:多个独立系统(CRM/ERP/数据平台)各自登录、各自样式,用户来回跳转。

微前端的答案:把一个巨石应用拆成多个独立开发、独立部署、独立技术栈的子应用,由一个基座(主应用)统一加载、路由分发、沙箱隔离。整体体验像一个应用,内部各自自治。

二、四大方案全景对比(面试常考)

维度qiankun(乾坤)micro-app(京东)wujie 无界(腾讯)Module Federation(Webpack5)
核心思路single-spa 扩展:HTML Entry + Proxy 沙箱Web Component 封装子应用iframe + Web Component(无 iframe 缺点)运行时模块共享(不是应用级)
JS 隔离Proxy 沙箱 / 快照沙箱Proxy 沙箱 + 严格模式iframe 天然隔离(真隔离)无(共享模块需自己约定)
样式隔离scoped 实验性 + 约定Shadow DOM 可选Web Component + 样式注入无(靠 CSS Modules)
通信props / 全局状态(redux)CustomEvent + 数据通信window 代理 + props共享模块(双向)
技术栈主流框架皆可主流框架皆可主流框架皆可需统一 Webpack5
上手成本中(改 entry 为 umd)低(侵入小)低(侵入最小)中(配置复杂)
适用场景中后台系统整合快速接入存量系统强隔离、性能敏感多团队共享组件/库
一句话区分:qiankun/micro-app/wujie 是把整个应用装进基座;Module Federation 是让应用之间共享模块(组件/工具库),两者可以组合用。

三、核心原理 ①:JS 沙箱(源码级)

为什么需要沙箱?

子应用在基座里运行,JS 全局变量(window.foo、挂到 window 的库)会互相污染:子应用 A 设置了全局,子应用 B 读到脏数据,卸载 A 后 B 的依赖也没了。

方案 1:快照沙箱(legacySandbox,兼容老浏览器)

class SnapshotSandbox {
  constructor() {
    this.modifiedProps = {};   // 记录改动
    this.active = false;
  }
  active() {
    // 激活时:把 window 现状存快照
    this.windowSnapshot = {};
    for (const key of Object.keys(window)) {
      this.windowSnapshot[key] = window[key];
    }
    // 恢复上次退出时的修改
    Object.keys(this.modifiedProps).forEach(k => { window[k] = this.modifiedProps[k]; });
  }
  inactive() {
    // 退出时:记录修改,还原 window 快照
    for (const key of Object.keys(window)) {
      if (window[key] !== this.windowSnapshot[key]) {
        this.modifiedProps[key] = window[key];   // 存下我改的
        window[key] = this.windowSnapshot[key];  // 还原成原来
      }
    }
  }
}

缺点:切换时全量遍历 window 属性,性能差(几毫秒级可接受但不优雅)。

四、核心原理 ②:Proxy 沙箱(现代主流,源码级)

思路:不碰真实 window,给子应用一个"虚拟 window"

class ProxySandbox {
  constructor() {
    const proxy = window;   // 最终会替换为 fakeWindow
    this.fakeWindow = {};
    this.active = true;
    // 拦截所有属性的读写:读不到时查真实 window,写入进 fakeWindow
    const target = {};
    return new Proxy(target, {
      get(_, key) {
        // 先从 fake 找,找不到回退真实 window
        if (key in target) return target[key];
        return window[key];
      },
      set(_, key, value) {
        target[key] = value;   // 子应用的全局变量写进"自己的沙箱"
        return true;
      },
    });
  }
}
// 子应用代码里 window.a = 1 —— 实际写到沙箱 target,不影响真实 window
// 卸载子应用时整个 proxy 丢弃,零残留 —— 这是 Proxy 沙箱完胜快照的原因
记忆点:快照沙箱 = 记账还原(全量遍历);Proxy 沙箱 = 白纸隔离(虚拟 window)。qiankun 默认用 Proxy,window.xxx 会被自动拦截,不需要子应用改代码。

五、核心原理 ③:样式隔离

问题:子应用 A 的 body { color: red } 会污染基座和子应用 B

方案 1:scoped(约定 + 编译期加前缀,qiankun 实验性)

// 编译期:给所有选择器加应用前缀(或约定子应用只写带前缀的类名)
// .btn {} → .app-a .btn {}
// 缺点:漏网之鱼(body/全局样式)仍会泄漏,需配合约定

方案 2:Shadow DOM(micro-app / wujie 用)—— 真隔离

const host = document.createElement('micro-app-a');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `<style>.btn{color:red}</style><div class="btn">A 应用</div>`;
// shadow 内的样式与外部完全隔离,外部选不中里面,里面选不中外面
// 缺点:弹层/全局类库(如 antd modal 挂 body 下)会逃出 shadow,需要挂载点处理

方案 3:动态样式处理(qiankun 常用)—— 卸载时移除 style 标签

子应用挂载时把它的 <style> 统一收集,卸载时移除,激活时重新注入 —— 避免"脏样式"累积。仍无法阻止运行时互相影响,所以实践上常配 scoped。

六、应用通信机制

方案机制适用
props 透传基座加载子应用时传入(含方法)基座 → 子应用
全局事件CustomEvent / mitt 事件总线松耦合广播
共享状态库基座提供全局 store(如用 zustand 单例)复杂业务状态
URL 路由参数跳转子应用路由带参页面间导航
Module Federation共享模块双向引用跨应用复用组件/库
原则:能走 URL 就走 URL,其次事件,最后才是共享状态 —— 通信越少,子应用越独立,拆得越干净。

七、优缺点与选型决策树

共同优点

共同缺点(面试必答)

选型决策树

  1. 子应用是"整个系统整合"且要渐进迁移? → qiankun
  2. 存量系统多、想侵入最小、快速接入? → micro-app(或 wujie)
  3. 强隔离要求(安全/复杂样式冲突)? → wujie(iframe 真隔离)
  4. 多团队共享组件/工具库、bundle 瘦身? → Module Federation(可叠加)
  5. 项目就一个应用、团队就一两个人? → 不要用微前端(复杂是负债)

八、案例解析:CRM + 数据平台 + 审批流三合一

背景

公司有三个独立系统:CRM(Vue2)、数据平台(React18)、审批流(老 jQuery)。员工每天切换登录、跳转体验割裂。目标是:统一登录、统一导航、整体像"一个工作台",但不重写任何老系统。

方案(qiankun 基座)

  1. 新写一个"工作台"基座(React + antd),左侧统一导航,顶部统一用户信息
  2. 三个老系统改造 entry 为 UMD 导出生命周期(bootstrap/mount/unmount),其余代码零改动
  3. 路由分发:/crm/* 挂 CRM,/data/* 挂数据平台,/approve/* 挂审批流
  4. 登录态:基座统一登录拿 token,通过 props 传给子应用,子应用删除各自登录页
// 老系统(Vue2 CRM)改造的最小改动:main.js 加三行
import { createApp } from 'vue';
import App from './App.vue';

let app = null;
export async function bootstrap() {}   // 生命周期①
export async function mount(props) {      // 生命周期②:基座调用
  app = createApp(App).mount(props.container.querySelector('#app'));
}
export async function unmount() {          // 生命周期③:切走时销毁
  app.$destroy(); app = null;
}

结果与复盘

九、坑清单(一线浓缩)

坑 1:子应用全局变量互相污染 —— 库挂 window(window.xxx = xxx)不写沙箱。解法:检查子应用 window 写入,全部走沙箱(Proxy 默认拦截)或改用 import。
坑 2:样式互相污染 —— 子应用写 body/ul/div 裸选择器。解法:scoped + 约定所有子应用类名带前缀,或 Shadow DOM。
坑 3:路由冲突 —— 基座与子应用都用 vue-router hash 模式,URL 解析错乱。解法:子应用路由 base 设为自身前缀,用 history 模式。
坑 4:卸载不干净 —— 子应用切走但定时器/全局监听/WebSocket 没清理,内存泄漏。解法:unmount 里清定时器、removeEventListener、关闭连接。
坑 5:公共依赖重复加载 —— 每个子应用都带一份 React,首屏 5MB。解法:externals + CDN 或 Module Federation 共享。
坑 6:弹层逃逸 —— Modal 挂 body,样式/层级失控。解法:统一配置 popup 挂载点。
坑 7:登录态不一致 —— 子应用各自管 token,401 后互相踢。解法:基座统一登录 + 401 统一处理。
坑 8:微前端化"过度设计" —— 明明一个应用能搞定,硬拆 8 个子应用,运维爆炸。解法:按"团队边界"拆,不按页面拆。

十、实战项目:基座 + 2 个子应用

要求(用 qiankun 或 micro-app)

  1. 基座(React):左侧菜单,路由分发 /app1/*/app2/*
  2. 子应用 1(Vue3):商品管理页;子应用 2(React):订单管理页(技术栈异构!)
  3. 基座给子应用传登录用户信息,子应用展示在页头
  4. 两个子应用各写一条"裸选择器全局样式",验证隔离是否生效

通关标准

十一、通关自检题

Q1:快照沙箱和 Proxy 沙箱的区别?
展开答案
快照 = 激活时存快照、退出时还原 window,全量遍历性能差;Proxy = 给子应用虚拟 window,写入进沙箱、读取回退真 window,卸载即丢弃零残留。
Q2:Shadow DOM 隔离样式为什么还有弹层问题?
展开答案
因为 antd/Element 等组件库的弹层(Modal/Dropdown)默认挂到 body 下,逃出 shadow 边界,样式失效或被外部污染;需配置弹层挂载点指向 shadow 内。
Q3:什么时候不应该用微前端?
展开答案
单团队小应用、无技术栈异构需求、无独立发布需求时,微前端引入的沙箱/通信/运维复杂度是纯负债。
Q4:Module Federation 和微前端是一回事吗?
展开答案
不是。微前端解决"应用级集成与隔离",MF 解决"模块级共享"(运行时加载别的应用的组件/库),可配合使用。
Q5:微前端首屏变慢怎么优化?
展开答案
预加载(空闲时预取子应用资源)、公共依赖 external + CDN、MF 共享框架、子应用按路由懒加载、SSR 化首屏子应用。