【5】微前端知识点总结

11 阅读12分钟

一、什么是微前端

微前端借鉴后端微服务理念,将前端单体应用拆分为多个可独立开发、独立部署、独立运行的子应用,再通过主应用统一整合展示。

核心特征:

  • 技术栈无关,子应用可自由选择框架(React、Vue、Angular 等)
  • 独立开发,各团队在独立仓库互不干扰
  • 独立部署,每个子应用单独构建发布
  • 运行时集成,子应用动态加载而非构建时打包在一起
  • 样式与 JS 隔离,子应用间互不污染

解决的核心问题:

  • 巨石应用难以维护:代码量膨胀,构建缓慢,模块耦合严重
  • 多团队协作冲突:共同维护一个仓库,代码冲突频繁,发布相互阻塞
  • 技术栈升级困难:整个应用绑定同一技术栈,无法局部渐进升级
  • 部署耦合:任何小改动都需重新构建部署整个应用,风险高
  • 历史系统整合:新旧系统无法共存,完全重写成本极高

二、技术实现方案

iframe

最简单的隔离方案,通过 <iframe> 标签嵌入子应用。浏览器天然提供 JS 和 CSS 的完全隔离,但 UI 体验差(弹窗、滚动、路由同步问题),通信只能依赖 postMessage,且每次加载都是全新页面,性能较差。

<!-- 主应用嵌入子应用 -->
<iframe src="https://sub-app.example.com" style="width:100%;height:100%;border:none;"></iframe>

<script>
  // 主应用向子应用发送消息
  document.querySelector('iframe').contentWindow.postMessage({ type: 'TOKEN', token: 'xxx' }, '*');

  // 子应用接收主应用消息
  window.addEventListener('message', (e) => {
    if (e.data.type === 'TOKEN') console.log(e.data.token);
  });
</script>

Web Components

利用浏览器原生 Custom Elements 将子应用封装为自定义组件,Shadow DOM 提供天然样式隔离。属于浏览器标准能力,无需额外框架,但生态尚不成熟,与主流框架集成有一定成本,IE 兼容性差。

class SubApp extends HTMLElement {
  connectedCallback() {
    // Shadow DOM 内部样式与外部完全隔离
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>.title { color: red; }</style>
      <div class="title">子应用内容</div>
    `;
  }
  disconnectedCallback() {
    // 组件卸载时清理资源
  }
}

customElements.define('sub-app', SubApp);
<!-- 主应用中像使用普通标签一样嵌入 -->
<sub-app></sub-app>

NPM 包

将子应用打包为 NPM 包由主应用引入,属于构建时集成。优点是简单直接、TypeScript 支持好,缺点是无法独立部署,版本升级需要主应用重新发布,不能做到真正的技术栈无关。

# 子应用发布为 npm 包
npm publish @company/sub-app

# 主应用安装依赖
npm install @company/sub-app
// 主应用直接 import,构建时打包在一起
import SubAppPage from '@company/sub-app';

// 子应用升级后,主应用必须更新依赖版本并重新构建部署

模块联邦(Module Federation)

Webpack 5 内置特性,支持在运行时跨应用共享模块。可以按模块粒度共享,避免重复加载公共依赖。强依赖 Webpack 5,沙箱隔离能力较弱,应用级别的路由管理需要自行实现。

// 子应用 webpack.config.js —— 暴露模块
new ModuleFederationPlugin({
  name: 'subApp',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button',
    './utils': './src/utils',
  },
  shared: ['vue'], // 声明共享依赖,避免重复加载
});

// 主应用 webpack.config.js —— 消费模块
new ModuleFederationPlugin({
  name: 'host',
  remotes: {
    subApp: 'subApp@http://localhost:3001/remoteEntry.js',
  },
});
// 主应用中运行时动态加载子应用暴露的模块
const Button = React.lazy(() => import('subApp/Button'));

JS 沙箱

通过代理或快照机制隔离子应用对全局 window 的读写,防止全局变量污染。是当前微前端框架实现 JS 隔离的核心技术手段,具体分为:

  • 快照沙箱:激活时对 window 拍快照,卸载时 diff 还原。简单但性能差,不支持多实例并行。
  • 代理沙箱(Proxy):基于 ES6 Proxy 拦截 window 操作,每个子应用维护独立的虚拟 window,读写互不干扰,支持多实例并行,是现代浏览器下的推荐方式。
// 快照沙箱:激活时保存快照,卸载时还原
class SnapshotSandbox {
  activate() {
    this.snapshot = Object.assign({}, window); // 保存当前 window 快照
  }
  deactivate() {
    for (const key in window) {
      if (window[key] !== this.snapshot[key]) {
        window[key] = this.snapshot[key]; // 还原所有变更
      }
    }
  }
}

// 代理沙箱:每个子应用独立的虚拟 window,互不污染
class ProxySandbox {
  constructor() {
    const fakeWindow = Object.create(null);
    this.proxy = new Proxy(fakeWindow, {
      set(target, prop, value) {
        target[prop] = value;          // 只写虚拟 window,真实 window 不变
        return true;
      },
      get(target, prop) {
        return prop in target ? target[prop] : window[prop]; // 取不到再读真实 window
      },
    });
  }
}

// 多子应用并行效果
// 子应用A: proxy.foo = 'A'  →  fakeWindowA.foo = 'A'
// 子应用B: proxy.foo = 'B'  →  fakeWindowB.foo = 'B'
// 真实 window.foo           →  undefined(始终干净)

2.6 CSS 隔离

防止子应用样式污染主应用或其他子应用,主要有三种方案:

  • 动态样式表:子应用激活时注入样式,卸载时移除。实现简单,但多应用并行展示时会冲突。
  • Shadow DOM:将子应用挂载在 Shadow DOM 内,浏览器原生保证内外样式完全隔离。隔离彻底,但挂载到 document.body 的弹窗、Tooltip 等组件样式会丢失,与主流 UI 库兼容性差。
  • Scoped CSS:运行时为子应用每条 CSS 规则动态添加属性选择器前缀(如 div[data-qiankun="app-name"]),实现作用域限定。兼容性好,但动态计算有一定性能开销,无法阻止主应用样式影响子应用。
// 动态样式表:挂载/卸载时插入和移除 <style> 标签
function mountStyles(cssTexts) {
  return cssTexts.map(css => {
    const el = document.createElement('style');
    el.textContent = css;
    document.head.appendChild(el);
    return el;
  });
}
function unmountStyles(styleEls) {
  styleEls.forEach(el => el.remove());
}

// Shadow DOM:子应用 DOM 和样式在 shadow-root 内完全隔离
const shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = `<style>${subAppStyles}</style>${subAppHTML}`;

// Scoped CSS:运行时动态为每条规则加属性前缀
function scopeCSS(cssText, appName) {
  // .btn { color: red }  →  div[data-qiankun="app"] .btn { color: red }
  return cssText.replace(/(^|\})\s*([^{]+)\{/g, (_, prefix, selector) => {
    const scoped = selector.split(',')
      .map(s => `div[data-qiankun="${appName}"] ${s.trim()}`)
      .join(', ');
    return `${prefix} ${scoped} {`;
  });
}

三、微前端产品方案

qiankun

蚂蚁金服出品,基于 single-spa 封装,是目前社区最成熟的微前端框架。

核心技术:

  • HTML Entry:以子应用 index.html 为入口,由 import-html-entry 解析并加载资源,子应用无需改造构建配置
  • JS 隔离:现代浏览器默认使用 ProxySandbox(多实例代理沙箱),每个子应用拥有独立虚拟 window;不支持 Proxy 时降级为 SnapshotSandbox
  • CSS 隔离:支持 Shadow DOM(strictStyleIsolation)和 Scoped CSS(experimentalStyleIsolation)两种模式,默认使用动态样式表
  • 应用通信:props 单向传递 + initGlobalState 全局状态管理
  • 预加载:支持在空闲时预加载子应用资源,加快激活速度

适合场景: 追求稳定大生态、团队规模较大的企业级中后台系统。


无界(Wujie)

腾讯出品,将 iframe 和 Web Components 结合的创新方案。

核心技术:

  • JS 隔离:复用 iframe 的 JS 运行环境作为天然沙箱,彻底隔离,无需手动实现代理沙箱
  • CSS 隔离:子应用 DOM 渲染在 Web Components 的 Shadow DOM 内,样式天然隔离
  • 应用保活:子应用切换时不销毁实例,保留 DOM 和状态,再次进入时秒开
  • 预加载:支持子应用预加载和后台静默运行

适合场景: 对隔离性要求极高、需要应用保活(切换不重载)的场景。


MicroApp(京东)

京东出品,基于 Web Components 封装,接入方式最简单。

核心技术:

  • JS 隔离:自研 Proxy 沙箱,与 qiankun 类似但实现更轻量
  • CSS 隔离:自动为子应用样式添加 CSS 作用域前缀
  • 接入方式:以自定义 HTML 标签 <micro-app> 的形式嵌入,对主应用几乎零侵入
  • Vite 支持:原生支持 Vite 构建的子应用,qiankun 对 Vite 支持较弱

适合场景: 希望以最低成本快速接入微前端、或子应用使用 Vite 构建的项目。


Module Federation(Webpack 5)

Webpack 内置能力,严格来说是模块共享方案而非完整微前端框架。

核心技术:

  • 运行时模块共享:应用间可以在运行时互相暴露和消费模块,避免公共依赖重复打包
  • 去中心化:每个应用既可以是 Host(消费方)也可以是 Remote(提供方),无需统一主应用
  • 无沙箱:没有内置 JS 和 CSS 隔离机制,需要开发者自行约束

适合场景: 深度绑定 Webpack 5、更关注模块级别共享而非应用级别隔离的场景。


产品方案对比

方案JS 隔离CSS 隔离接入成本Vite 支持社区生态
qiankunProxy 沙箱Shadow DOM / Scoped较差⭐⭐⭐⭐⭐
无界iframe 天然隔离Shadow DOM⭐⭐⭐
MicroAppProxy 沙箱Scoped CSS极低⭐⭐⭐
Module Federation❌ 无❌ 无⭐⭐⭐

四、qiankun 核心技术详解

JS 沙箱

SnapshotSandbox(快照沙箱)

激活时遍历 window 保存快照,卸载时对比快照还原所有变更。实现简单,但每次激活/卸载都需遍历整个 window,性能较差,且同一时刻只能运行一个子应用。

class SnapshotSandbox {
  activate() {
    this.windowSnapshot = {};
    for (const key in window) {
      this.windowSnapshot[key] = window[key];
    }
    // 恢复上次该沙箱运行时的修改
    Object.keys(this.modifyPropsMap).forEach(key => {
      window[key] = this.modifyPropsMap[key];
    });
  }

  deactivate() {
    this.modifyPropsMap = {};
    for (const key in window) {
      if (window[key] !== this.windowSnapshot[key]) {
        this.modifyPropsMap[key] = window[key]; // 记录变更
        window[key] = this.windowSnapshot[key]; // 还原
      }
    }
  }
}

ProxySandbox(多实例代理沙箱)

每个子应用拥有独立的 fakeWindow,所有对 window 的读写都发生在各自的 fakeWindow 上,真实 window 始终保持干净,天然支持多实例并行。

class ProxySandbox {
  constructor() {
    const fakeWindow = Object.create(null);

    this.proxy = new Proxy(fakeWindow, {
      set(target, prop, value) {
        target[prop] = value; // 只写入虚拟 window
        return true;
      },
      get(target, prop) {
        // 优先读虚拟 window,取不到再读真实 window
        return prop in target ? target[prop] : window[prop];
      },
      has(target, prop) {
        return prop in target || prop in window;
      }
    });
  }
}

多实例并行效果:

子应用A: window.foo = 'A'  →  fakeWindowA.foo = 'A'
子应用B: window.foo = 'B'  →  fakeWindowB.foo = 'B'
真实 window.foo            →  undefined(完全干净)

沙箱盲区: JS 沙箱只拦截 window 属性读写,对 document.body.appendChild、setTimeout、addEventListener 等操作无能为力,子应用卸载时需在 unmount 钩子中手动清理,否则会内存泄漏。


CSS 隔离

Shadow DOM(strictStyleIsolation)

start({ sandbox: { strictStyleIsolation: true } });

子应用的 DOM 被挂载在 Shadow Root 内,浏览器原生保证边界内外样式互不穿透。隔离最彻底,但挂载到 document.body 的弹窗、下拉菜单等组件会逃出 Shadow DOM 边界导致样式丢失,需要手动将这类组件的挂载节点指定到子应用容器内。

主应用 DOM
└── #micro-container
    └── shadow-root        ← 样式边界
        ├── <style>子应用样式</style>
        └── <div id="app">子应用内容</div>

Scoped CSS(experimentalStyleIsolation)

start({ sandbox: { experimentalStyleIsolation: true } });

qiankun 拦截子应用的样式注入,在运行时为每条 CSS 规则动态添加属性选择器前缀,将样式的作用域限定在子应用容器内:

/* 原始 */
.btn { color: red; }

/* 处理后 */
div[data-qiankun="vue-app"] .btn { color: red; }

兼容性好,弹窗问题少,是日常更推荐的方案。但动态计算有性能开销,且无法阻止主应用样式向下影响子应用。


CSS 隔离方案对比:

方案隔离方向弹窗兼容推荐场景
动态样式表(默认)子应用间不同时存在基础场景
Shadow DOM双向完全隔离❌ 需额外处理隔离要求极高
Scoped CSS子应用不影响外部日常推荐

4.3 应用间通信

子应用间的通信分为三种场景:主应用向子应用传递数据、子应用向主应用反馈、子应用之间互相通信。

props 传递

最简单直接的方式,主应用在注册子应用时通过 props 字段传入数据或回调函数。子应用在 mount 钩子中接收。这种方式是单向的,适合传递初始配置、用户信息、或让子应用调用主应用提供的方法(如全局登出)。

// 主应用
registerMicroApps([{
  name: 'sub-app',
  props: {
    token: 'xxx',
    userInfo: { name: 'John' },
    onLogout: () => { /* 主应用处理登出逻辑 */ }
  }
}]);

// 子应用 mount 钩子中接收
export async function mount(props) {
  const { token, userInfo, onLogout } = props;
}

initGlobalState(全局状态)

qiankun 内置的发布订阅机制,主应用初始化一个全局状态对象,主应用和所有子应用都可以监听状态变化、也可以更新状态。适合需要跨应用共享且频繁变化的数据,如当前用户信息、主题、语言等。

需要注意的是,子应用只能调用 setGlobalState 修改已存在的一级属性,不能新增顶层字段,状态的结构由主应用初始化时决定。

// 主应用初始化
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, theme: 'light' });

actions.onGlobalStateChange((state, prev) => {
  console.log('状态变更:', prev, '→', state);
});

// 子应用中(通过 mount props 获取 actions)
export async function mount(props) {
  props.onGlobalStateChange((state) => {
    console.log('子应用收到状态:', state);
  });
  props.setGlobalState({ theme: 'dark' }); // 触发所有监听者
}

自定义事件总线

当需要子应用之间直接通信,而不必经过主应用中转时,可以在主应用初始化时挂载一个全局事件总线,所有子应用共享使用。这种方式灵活性最高,但需要注意子应用卸载时要及时 off 事件,避免监听器堆积。

// 主应用初始化,挂载到全局
class EventBus {
  constructor() { this.events = {}; }
  on(event, fn) { (this.events[event] ??= []).push(fn); }
  emit(event, data) { (this.events[event] ?? []).forEach(fn => fn(data)); }
  off(event, fn) { this.events[event] = (this.events[event] ?? []).filter(f => f !== fn); }
}
window.__BUS__ = new EventBus();

// 子应用A 发送
window.__BUS__.emit('order:created', { id: 123 });

// 子应用B 接收(unmount 时记得 off)
window.__BUS__.on('order:created', handler);

4.4 生命周期

子应用需要导出三个生命周期钩子供 qiankun 调用:

钩子触发时机调用次数常见用途
bootstrap资源加载完成后,首次激活前仅一次初始化全局配置
mount路由匹配,子应用激活多次渲染应用、绑定事件
unmount路由离开,子应用卸载多次销毁实例、清理定时器和事件监听
首次进入:bootstrap → mount
路由切换:unmount → mount(重复)

五、微前端优势

  • 技术栈自由:各子应用独立选型,新技术可在单个子应用中试用,不影响整体
  • 独立部署:子应用单独构建发布,发布频率和节奏互不干扰
  • 团队自治:团队边界与应用边界对齐,减少跨团队协作摩擦
  • 渐进式迁移:可将旧系统逐模块替换为新技术栈,无需一次性重写
  • 故障隔离:单个子应用崩溃不影响主应用和其他子应用
  • 按需加载:用户只加载当前访问模块,首屏资源体积更小

六、参考资料