架构模式

微前端架构
深度解析

从 JS 沙箱原理到 CSS 隔离策略,从依赖共享方案到技术选型决策 —— 全面拆解微前端的核心隔离机制与工程化实践。

沙盒隔离
CSS 隔离
依赖共享
技术选型
核心挑战

微前端要解决的三大问题

JS 沙箱隔离

多个子应用共享同一个全局 window,如何防止全局变量污染和冲突?

CSS 样式隔离

不同子应用的 CSS 选择器可能重名,如何保证样式互不干扰?

公共依赖共享

每个子应用都打包 React/Vue 会导致资源浪费,如何高效共享?

Part 01

JS 沙箱方案

沙箱(Sandbox)是微前端的核心能力,它为每个子应用创建一个隔离的 JavaScript 执行环境,防止子应用修改全局对象导致其他应用出错。

iframe 沙盒

隔离强度
核心原理

利用浏览器原生 iframe 标签实现完全隔离。每个子应用运行在独立的浏览上下文中,拥有独立的 window、document 和全局作用域。

实现机制
1
主应用创建 iframe 元素,指向子应用 URL
2
子应用在 iframe 内部独立运行,拥有完整的 JS/CSS 环境
3
通过 postMessage / CustomEvent 进行跨 iframe 通信
4
可监听 iframe load 事件感知子应用生命周期
优势
  • +隔离性最强,CSS/JS 完全独立
  • +无需任何 JS 沙箱改造
  • +天然支持多实例并行运行
  • +子应用可使用任意前端框架
劣势
  • !URL 状态无法自动同步(路由割裂)
  • !弹窗/遮罩层无法突破 iframe 边界
  • !DOM 不共享,通信成本高
  • !性能开销较大,每次加载完整文档
  • !SEO 不友好
查看代码示例
Implementation
// 主应用创建 iframe
const iframe = document.createElement('iframe');
iframe.src = 'https://sub-app.example.com';
iframe.style.cssText = 'width:100%;height:100%;border:none;';
container.appendChild(iframe);

// 通信
iframe.contentWindow.postMessage(
  { type: 'INIT', data: { user, token } },
  'https://sub-app.example.com'
);

window.addEventListener('message', (e) => {
  if (e.origin === 'https://sub-app.example.com') {
    console.log('收到子应用消息:', e.data);
  }
});
适用场景:对隔离要求极高的场景,如接入第三方不可信应用

快照沙盒 (Snapshot Sandbox)

隔离强度
核心原理

在子应用激活时恢复全局状态快照,在卸载时保存当前状态快照。通过 diff 比较实现状态的保存与恢复。

实现机制
1
激活时:遍历快照中的属性,还原到 window 上
2
运行时:子应用对 window 的修改直接生效
3
卸载时:遍历 window 属性,与激活前快照做 diff
4
将 diff(变更部分)保存为新的快照
优势
  • +兼容性极好,支持 IE 等旧浏览器
  • +实现相对简单,易于理解
  • +子应用无感知,无需改造
劣势
  • !同一时刻只能运行一个子应用
  • !快照/恢复性能损耗随 window 属性增多而增大
  • !无法处理 window 上的 getter/setter
  • !可能存在状态残留
查看代码示例
Implementation
class SnapshotSandbox {
  constructor() {
    this.snapshot = {};
    this.modifyPropsMap = {};
  }
  // 激活:恢复快照
  active() {
    // 恢复上次保存的状态
    for (const key in this.snapshot) {
      window[key] = this.snapshot[key];
    }
  }
  // 卸载:保存快照
  inactive() {
    // 记录当前 window 状态
    for (const key in window) {
      if (window[key] !== this.snapshot[key]) {
        this.modifyPropsMap[key] = window[key];
        // 还原为激活前的状态
        window[key] = this.snapshot[key];
      }
    }
    // 合并修改到快照
    this.snapshot = { ...this.snapshot, ...this.modifyPropsMap };
  }
}
适用场景:需要兼容老旧浏览器,且子应用不需要同时运行的场景

代理沙盒 (Proxy Sandbox)

隔离强度
核心原理

使用 ES6 Proxy 创建 fakeWindow 代理对象。子应用的所有全局变量读写操作都映射到各自的 fakeWindow 上,互不干扰。

实现机制
1
为每个子应用创建独立的 fakeWindow 对象
2
使用 Proxy 拦截 window 的 get/set 操作
3
set 操作只修改 fakeWindow,不影响真实 window
4
get 操作优先从 fakeWindow 取值,再 fallback 到 window
优势
  • +支持多实例同时运行(多代理并存)
  • +不修改真实 window,无副作用
  • +可精确记录子应用的全局变量修改
  • +支持白名单机制,可配置可逃逸的变量
劣势
  • !不支持 IE 浏览器(Proxy 是 ES6 特性)
  • !部分 DOM API 在 Proxy 下可能行为异常
  • !with 语句配合 Proxy 有一定性能开销
  • !无法完全拦截所有 window 属性(如 window.window)
查看代码示例
Implementation
class ProxySandbox {
  constructor(name) {
    this.name = name;
    this.fakeWindow = {};
    // 记录所有被 set 过的属性
    this.updatedValueSet = new Set();

    this.proxy = new Proxy(window, {
      get: (target, key) => {
        // 优先从 fakeWindow 取值
        if (key in this.fakeWindow) {
          return this.fakeWindow[key];
        }
        const val = target[key];
        // 绑定正确的 this 指向
        return typeof val === 'function'
          ? val.bind(target) : val;
      },
      set: (_, key, value) => {
        this.fakeWindow[key] = value;
        this.updatedValueSet.add(key);
        return true;
      },
      has: (_, key) => {
        return key in this.fakeWindow || key in window;
      },
    });
  }
}
适用场景:qiankun 默认方案,需要多子应用并行运行的主流场景

VM 沙盒 (V8 Sandbox)

隔离强度
核心原理

利用浏览器端的 JavaScript 解析/执行能力,将子应用代码包裹在自定义的执行上下文中。通过 new Function 或自定义模块加载器实现。

实现机制
1
将子应用 JS 代码作为字符串获取
2
构造自定义的执行上下文(with 语句 + Proxy)
3
使用 new Function 或 eval 在受限环境中执行代码
4
通过 Proxy 拦截所有自由变量的访问
优势
  • +隔离粒度最细,可控制到变量级别
  • +可实现模块级别的沙箱隔离
  • +支持精确的依赖注入
  • +体积小,启动速度快
劣势
  • !with + new Function 存在 CSP 安全策略限制
  • !调试困难,源码映射复杂
  • !部分浏览器 API 调用可能失效
  • !动态执行代码的性能开销
查看代码示例
Implementation
// micro-app 的沙箱实现思路
function createVmSandbox(code, context) {
  // 使用 with 语句将 context 作为作用域链
  const fn = new Function(
    'window', 'self', 'globalThis',
    `with(window) {
      try { ${code} }
      catch(e) { console.error(e); }
    }`
  );

  // Proxy 包装 context
  const proxy = new Proxy(context, {
    has: () => true,  // 所有变量都从 context 查找
    get: (target, key) => target[key] ?? window[key],
    set: (target, key, value) => {
      target[key] = value;
      return true;
    }
  });

  fn(proxy, proxy, proxy);
}
适用场景:micro-app 默认方案,需要极致轻量和精细隔离的场景

沙箱方案对比速查表

方案隔离强度多实例IE 兼容性能代表框架
iframe★★★★★⚠️ 较差无界(wujie)
快照沙盒★★★☆☆⚡ 良好qiankun legacy
代理沙盒★★★★☆⚡ 良好qiankun proxy
VM 沙盒★★★★☆⚡ 良好micro-app
Part 02

CSS 隔离方案

样式冲突是微前端最常遇到的问题之一。以下方案按隔离强度从高到低排列,各有其适用场景。

Shadow DOM

最强

利用浏览器原生的 Shadow DOM 能力,将子应用的 DOM 和样式完全封装在 Shadow Root 内部,外部样式无法侵入,内部样式也不会泄漏。

实现细节
attachShadow({ mode: 'open' }) 创建影子根节点
外部 CSS 选择器无法选中 Shadow 内部元素
内部 <style> 标签仅作用于 Shadow 范围
可通过 CSS Custom Properties (--var) 向内传递变量
✓ 优势
+浏览器原生隔离,最彻底
+无命名冲突风险
+无需额外工具链
✗ 劣势
-弹窗/下拉菜单可能逃逸出 Shadow 边界
-第三方 UI 库可能不兼容
-调试工具支持有限
-focus/tabindex 等可访问性可能受影响
代码示例
// micro-app 默认使用 Shadow DOM
class MicroApp extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    // 子应用 HTML 被注入到 Shadow 内部
    shadow.innerHTML = '<div id="app-root"></div>';
    // 子应用的样式也被注入到 Shadow 内部
    // 外部样式完全不影响子应用
  }
}

Scoped CSS(动态作用域)

在运行时动态为子应用的所有样式规则添加唯一的作用域前缀(如 [data-app-id='vue-app']),使样式仅匹配对应子应用的 DOM 节点。

实现细节
劫持 HTMLStyleElement.prototype 和 CSSStyleSheet
解析 CSS 规则,为选择器添加属性选择器限定
为子应用根节点添加唯一的 data-app-id 属性
处理 @media、@keyframes 等特殊规则
✓ 优势
+子应用无感知,无需改造
+不依赖 Shadow DOM,兼容性好
+支持大部分 CSS 语法
+运行时自动处理动态插入的样式
✗ 劣势
-正则解析 CSS 可能遗漏边界情况
-运行时性能损耗(样式越多越慢)
-无法处理 JS 动态创建的 style
-对 @import 和 CSS Variables 处理不完美
代码示例
// qiankun scopedCSS 实现思路
function scopedCSS(styleElement, appName) {
  // 劫持 sheet.cssRules
  const sheet = styleElement.sheet;
  const rules = sheet.cssRules;

  for (let i = 0; i < rules.length; i++) {
    const rule = rules[i];
    if (rule instanceof CSSStyleRule) {
      // 为选择器添加作用域限定
      // .btn → .btn[data-name="sub-app"]
      const selector = rule.selectorText;
      rule.selectorText = addScope(selector, appName);
    }
  }
}

function addScope(selector, appName) {
  return selector.split(',').map(s =>
    '[data-name="' + appName + '"] ' + s.trim()
  ).join(', ');
}

CSS Modules / CSS-in-JS

中强

在编译阶段通过 CSS Modules 的哈希命名或 CSS-in-JS 的运行时唯一类名生成,确保每个子应用的样式类名唯一,从根源上避免冲突。

实现细节
CSS Modules: 编译时为类名添加哈希后缀(.btn → .btn_a3x2f)
CSS-in-JS: 运行时生成唯一类名,注入 <style> 标签
styled-components / emotion 等方案自动添加唯一前缀
需要子应用在开发时采用对应方案
✓ 优势
+类名冲突概率趋近于零
+成熟的生态系统和工具链支持
+支持动态样式和主题切换
+CSS Modules 零运行时开销
✗ 劣势
-需要子应用主动改造,接入成本高
-全局样式(reset/normalize)仍需额外处理
-CSS-in-JS 有运行时性能开销
-服务端渲染场景配置复杂
代码示例
// CSS Modules 示例
// Button.module.css
.btn { background: var(--primary); }
.btn_active { background: var(--active); }

// Button.jsx - 编译后类名自动唯一化
import styles from './Button.module.css';
// 渲染为: <button class="btn_x8k2a btn_active_f3j9p">Click</button>
<button className={`${styles.btn} ${isActive && styles.btn_active}`}>

// styled-components 示例
const StyledBtn = styled.button`
  background: ${p => p.primary ? '#8B5CF6' : '#fff'};
  // 自动添加唯一哈希类名
`;

BEM 命名约定

通过严格的命名规范(Block__Element--Modifier),为每个子应用的所有 CSS 类名添加应用前缀,人为避免命名冲突。

实现细节
每个子应用使用唯一的 Block 前缀(如 app-vue-、app-react-)
类名格式: {app}-{block}__{element}--{modifier}
需要 ESLint/stylelint 规则强制约束
可以配合 PostCSS 插件自动添加前缀
✓ 优势
+零技术门槛,无需额外工具
+所有框架/构建工具均兼容
+易于理解和实施
+命名自带语义化
✗ 劣势
-依赖开发者自觉性,易出错
-全局样式(如 body/html)仍可能冲突
-类名冗长,编写和维护成本高
-无法防止第三方库样式冲突
代码示例
/* 每个子应用使用唯一前缀 */
/* 子应用 A */
.app-cart__header--active { color: #8B5CF6; }
.app-cart__item--selected { border: 2px solid #8B5CF6; }

/* 子应用 B */
.app-order__header--active { color: #F472B6; }
.app-order__item--selected { border: 2px solid #F472B6; }

/* PostCSS 自动添加前缀 */
// postcss.config.js
module.exports = {
  plugins: {
    'postcss-prefix-selector': {
      prefix: '.app-cart',
      transform(_, selector) {
        return '.app-cart ' + selector;
      }
    }
  }
};

💡 实践建议:组合使用

在实际项目中,通常组合使用多种 CSS 隔离方案。推荐策略:

推荐
高安全
Shadow DOM + Scoped CSS
传统
高兼容
Scoped CSS + BEM 前缀
便捷
零改造
框架内置自动处理
Part 03

依赖共享方案

如果每个子应用都独立打包 React、Vue 等基础库,将导致巨大的资源浪费和性能问题。 依赖共享是微前端工程化的核心优化手段。

共享 vs 不共享
App A
React × 3
App B
React × 3
App C
React × 3
❌ 不共享:重复加载,浪费带宽
总计加载 3 份 React(~300KB × 3)
React (共享层) × 1
App A
App B
App C
✅ 共享:按需复用,节省资源
总计仅加载 1 份 React(~100KB × 1)

Webpack Externals + Import Maps

主应用通过 script 标签全局加载公共依赖(React、Vue 等),子应用通过 Webpack Externals 配置排除这些依赖,运行时从全局获取。

实施步骤
1主应用在 HTML 中通过 <script> 引入公共依赖
2主应用配置 Import Maps 映射模块名到全局变量
3子应用配置 Webpack Externals 排除公共依赖
4子应用 import 语句在运行时解析为全局变量访问
实现简单,广泛使用
显著减少重复加载的资源体积
所有构建工具均支持
需要主/子应用约定相同的依赖版本
版本升级需要所有应用同步更新
全局变量管理容易混乱
Import Maps 浏览器兼容性有限(需 polyfill)
查看代码示例
Configuration
<!-- 主应用 HTML -->
<script type="importmap">
{
  "imports": {
    "react": "https://cdn.example.com/react@18.2.0/umd/react.min.js",
    "react-dom": "https://cdn.example.com/react-dom@18.2.0/umd/react-dom.min.js",
    "vue": "https://cdn.example.com/vue@3.3.0/dist/vue.global.prod.js"
  }
}
</script>

// 子应用 webpack.config.js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    vue: 'Vue'
  }
};

// 子应用代码正常 import,运行时使用全局变量
import React from 'react'; // → window.React

Module Federation(模块联邦)

Webpack 5 原生支持的模块共享方案。应用可作为远程模块暴露组件/工具,其他应用按需异步加载,实现真正的运行时模块共享。

实施步骤
1各应用在 webpack 配置中声明 exposes(暴露的模块)和 remotes(远程模块)
2构建后生成 remoteEntry.js 入口文件
3消费方通过 import() 动态加载远程模块
4shared 配置控制共享依赖的版本策略和加载方式
真正的运行时模块共享,无需预编译
支持独立部署,版本自动协商
可按需加载,性能优秀
共享依赖自动单例化
强依赖 Webpack 5(Vite 需要插件)
共享依赖版本管理复杂
运行时加载增加首屏延迟
调试和错误追踪困难
TypeScript 类型共享需额外配置
查看代码示例
Configuration
// App1(远程应用)- webpack.config.js
new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button',
    './utils': './src/utils/index',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
  },
});

// App2(消费应用)- webpack.config.js
new ModuleFederationPlugin({
  name: 'app2',
  remotes: {
    app1: 'app1@http://localhost:3001/remoteEntry.js',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
});

// App2 中使用远程组件
const RemoteButton = React.lazy(
  () => import('app1/Button')
);

SystemJS + UMD

SystemJS 作为通用模块加载器,可在浏览器中加载 UMD/AMD/System 模块格式。主应用使用 SystemJS 加载子应用,子应用输出为 UMD 格式实现依赖共享。

实施步骤
1子应用构建为 UMD 或 System 格式
2主应用使用 SystemJS.load() 加载子应用入口
3共享依赖通过 SystemJS import map 声明
4子应用的 import 语句由 SystemJS 在运行时解析
浏览器兼容性极好(支持 IE)
支持多种模块格式(UMD/AMD/ESM/System)
运行时动态加载
qiankun 内部使用的方案
需要额外引入 SystemJS 运行时(~15KB gzip)
模块格式转换增加加载延迟
调试体验差,错误堆栈复杂
ESM 原生支持已成主流,SystemJS 逐渐边缘化
查看代码示例
Configuration
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.min.js"></script>
<script>
  System.import('https://sub-app.example.com/main.js')
    .then(appModule => {
      appModule.bootstrap(document.getElementById('app'));
    });
</script>

// 子应用 webpack 配置 output.libraryTarget
module.exports = {
  output: {
    libraryTarget: 'system', // 或 'umd'
    filename: 'main.js',
  },
};

// 通过 importmap 共享依赖
<script type="systemjs-importmap">
{
  "imports": {
    "react": "https://cdn.example.com/react.production.min.js",
    "react-dom": "https://cdn.example.com/react-dom.production.min.js"
  }
}
</script>
Part 04

主流框架对比

了解了底层原理,让我们看看市面上主流的微前端框架是如何将这些方案组合使用的。

qiankun

稳定
沙箱方案
Proxy / Snapshot
CSS 隔离
Scoped CSS
依赖共享
externals + SystemJS
优势

生态成熟、社区活跃、文档完善

劣势

沙箱不够彻底、CSS 隔离有边界问题

micro-app

活跃
沙箱方案
VM Sandbox (iframe 降级)
CSS 隔离
Shadow DOM / Scoped CSS
依赖共享
预加载 + externals
优势

类 Web Component 接入、零改造、轻量

劣势

沙箱偶有边界 case、社区较小

wujie(无界)

活跃
沙箱方案
iframe + Proxy 混合
CSS 隔离
Shadow DOM + CSS Scope
依赖共享
iframe 通信 + 预加载
优势

iframe 隔离但无 URL 割裂问题、组件化接入

劣势

较新、生态待完善

Module Federation

标准
沙箱方案
无(应用自行隔离)
CSS 隔离
需自行处理
依赖共享
内置 shared 机制
优势

Webpack 5 原生、运行时共享、独立部署

劣势

不提供沙箱和路由、强绑定 Webpack

🎯 技术选型决策指南

需要接入第三方不可信应用?
iframe / wujie
iframe 天然隔离最强,适合不可信来源
需要多子应用同时运行?
qiankun Proxy / micro-app
Proxy 沙盒支持多实例并行
需要兼容 IE 浏览器?
qiankun Snapshot / iframe
快照沙盒不依赖 Proxy 等 ES6 特性
追求极致轻量零改造?
micro-app
类 Web Component 接入,子应用几乎无需改造
需要运行时模块共享?
Module Federation
Webpack 5 原生支持,真正的运行时共享
新项目技术栈统一?
pnpm monorepo
如果不需要技术栈隔离,monorepo 可能更简单

核心要点回顾

微前端的核心价值在于技术栈无关的独立部署能力。 沙箱隔离保证运行时安全,CSS 隔离保证视觉独立,依赖共享优化加载性能。 没有银弹方案,根据团队实际情况选择合适的组合才是关键。

沙箱:按隔离需求选择CSS:推荐 Shadow DOM + Scoped依赖:Module Federation 最灵活