微前端架构:组合、隔离与独立部署

微前端架构的工程实践:要解决的问题与适用边界、构建时与运行时两种集成方式的取舍、Module Federation 与微应用方案对比、JS 沙箱与 CSS 隔离机制、路由归属与状态共享原则、独立部署与版本回滚、可用性兜底,以及常见反模式与必须接受的真实代价。

当多个团队同时改一个前端仓库时,冲突、排队与"改一处、全量回归"会迅速吞噬交付效率。微前端(Micro Frontends)把后端微服务的自治思想搬到浏览器侧:让每个团队独立开发、独立部署自己负责的页面片段,再在运行时组合成一个完整应用。但浏览器不像服务端那样有天然的进程隔离,组合方式、样式冲突、状态共享都是硬骨头。本文讲清技术选型、隔离机制与真实代价。

1. 微前端要解决什么问题

1.1 单体前端的痛点

症状根因
发布排队所有团队共用一个仓库、一次发布
回归成本高改 A 模块要回归整个应用
技术栈锁死全站被迫用同一框架版本
构建越来越慢单仓体积膨胀,CI 时间线性增长

这些症状与后端 单体拆微服务 的动机如出一辙——用自治换取效率。

1.2 适用边界

微前端不是默认选项。判断标准:

值得做:多团队并行、模块业务边界清晰、发布节奏差异大、
        需要渐进式重构老前端(绞杀)
不值得:单团队、模块间强耦合、页面交互高度统一、
        团队规模小(引入的成本 > 收益)

最重要的反例:如果只是"想让代码组织更好",那用 monorepo + 合理的模块划分就够了,不需要微前端。


2. 集成方式:构建时 vs 运行时

2.1 构建时集成

把各模块作为 npm 包发布,主应用在构建时打包进来:

// 主应用 package.json
{
  "dependencies": {
    "@acme/header": "^2.3.0",
    "@acme/order-ui": "^1.8.0",
    "@acme/profile-ui": "^3.0.1"
  }
}
优点缺点
类型安全、可 tree-shaking无法独立部署,升级要重新构建主应用
依赖统一、无运行时开销版本冲突需集中协调
调试简单违背"独立部署"初衷

它其实是组件库模式,不是真正的微前端——只在"独立部署"确实不需要时才适用。

2.2 运行时集成:Module Federation

Webpack 5 的 Module Federation(模块联邦)是当前主流方案,允许一个应用在运行时加载另一个应用暴露的模块:

// 远程应用 order-ui/webpack.config.js —— 暴露模块
module.exports = {
  name: 'orderUi',
  filename: 'remoteEntry.js',
  exposes: {
    './OrderList': './src/OrderList',
    './OrderDetail': './src/OrderDetail',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
  },
};
// 主应用 webpack.config.js —— 消费远程模块
module.exports = {
  name: 'shell',
  remotes: {
    orderUi: 'orderUi@https://order.example.com/remoteEntry.js',
    profileUi: 'profileUi@https://profile.example.com/remoteEntry.js',
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
};

主应用动态加载远程模块:

import React, { Suspense, lazy } from 'react';

const OrderList = lazy(() => import('orderUi/OrderList'));

export function OrdersPage() {
  return (
    <Suspense fallback={<Spinner />}>
      <OrderList />
    </Suspense>
  );
}

shared 里把 React 标记为 singleton 是关键——否则每个远程模块各带一份 React,会出现"多个 React 实例"导致 hooks 报错。

2.3 iframe 与 Web Components

方案隔离强度集成体验适用
iframe最强(浏览器级)差(路由、弹窗、样式受限)强隔离需求、第三方嵌入
Web Components中(Shadow DOM)中(样式隔离好,事件通信弱)组件级复用
Module Federation弱(需自建沙箱)最好(同一文档)同技术栈、深度交互
qiankun/微应用中(运行时沙箱)较好异构技术栈并存

iframe 隔离最彻底但体验最差——URL 不同步、弹窗无法覆盖父页面、通信只能靠 postMessage。多数业务场景不推荐把整个子应用塞进 iframe。


3. 技术隔离:让多个应用共处一页

3.1 JS 沙箱

多个微应用在同一文档里跑,最大的风险是全局变量污染(window 上的属性互相覆盖)和事件监听泄漏。运行时沙箱通过代理 window 解决:

// 简化的 Proxy 沙箱思路(qiankun 风格)
class Sandbox {
  constructor() {
    this.proxy = new Proxy(window, {
      get: (target, key) => (key in this.modified ? this.modified[key] : target[key]),
      set: (target, key, value) => {
        if (!(key in target)) this.modified[key] = value;  // 记录新增属性
        else target[key] = value;
        return true;
      },
    });
  }
  // 卸载时还原被修改的全局变量
  unmount() {
    for (const key of Object.keys(this.modified)) delete window[key];
  }
}

切换微应用时执行 unmount() 清理副作用,避免上一个应用的定时器、事件监听继续运行。

3.2 CSS 隔离

样式冲突是微前端最容易被低估的坑:A 团队的 .button 覆盖了 B 团队的 .button。三种隔离手段:

手段原理成本
BEM/命名前缀约定 order-btn 前缀靠自觉,易破
CSS Modules / scoped编译期加哈希后缀需构建配合,全局样式难共享
Shadow DOM浏览器原生隔离弹窗定位、全局字体需额外处理
运行时样式隔离切换时挂载/卸载 <style>动态插入的样式难追踪

实践中常用组合拳:CSS Modules 做组件级隔离 + 约定前缀 + 设计 token 共享全局变量。

/* 设计 token 放在全局,用 CSS 变量共享 */
:root {
  --color-primary: #2563eb;
  --radius-md: 8px;
}

4. 路由与状态共享

4.1 路由归属

两种主流策略:

主应用持有路由(推荐):
  shell 负责 URL 解析,按前缀加载对应微应用
  /order/*   → orderUi
  /profile/* → profileUi
  微应用只处理自己前缀内的子路由

微应用自持路由:
  每个微应用管理完整路由,主应用只做容器
  优点:微应用可独立运行
  缺点:路由冲突、浏览器前进后退难协调

推荐主应用持有路由:URL 是全局资源,集中管理才能保证前进后退、深链接、刷新状态一致。

4.2 状态共享的克制原则

微前端之间不应共享业务状态——共享越多,耦合越强,独立部署就失去意义。只在必要时共享极少量全局状态:

该共享不该共享
当前用户/权限订单列表数据
主题/语言表单草稿
全局通知/Toast各模块的 loading 状态
埋点上下文业务实体缓存

用发布订阅或极简 store 共享这几项即可:

// 全局事件总线:只广播"发生了什么",不传业务数据
eventBus.emit('user:logout');
eventBus.on('theme:change', ({ theme }) => applyTheme(theme));

这其实与 BFF 与 API 网关 的边界思想一致:跨边界的契约越窄越稳。


5. 独立部署与版本管理

5.1 独立部署的真义

微前端最大的价值是部署解耦:orderUi 团队发版不需要 shell 重新构建、不需要其他团队协调。

orderUi 团队:改代码 → CI 构建 → 上传 remoteEntry.js → 生效
shell 团队:   什么都不用做

前提是接口稳定:远程模块的 props、事件、路由契约一旦约定,就要像 API 一样对待,破坏性变更需要版本化。

5.2 版本与回滚

# 远程模块清单:声明各微应用可用版本
micro_apps:
  orderUi:
    entry: https://order.example.com/remoteEntry.js
    version: 2.4.1
    contract: ">=2.0.0 <3.0.0"
  profileUi:
    entry: https://profile.example.com/remoteEntry.js
    version: 1.9.0
    contract: ">=1.5.0 <2.0.0"

回滚策略:远程入口 URL 带版本号,出问题时把清单指回上一个版本即可,无需重新部署主应用。

https://order.example.com/v2.4.1/remoteEntry.js  ← 回滚只需改这一个 URL

5.3 兼容性治理

跨微应用的契约变更要做灰度与双版本并存:

  • 新增 props:向后兼容,直接发;
  • 修改 props 语义:发布新主版本,主应用同时加载新旧入口,按灰度比例切流;
  • 移除 props:先在所有调用方迁移完,再下线旧版本。

6. 反模式与真实代价

6.1 反模式

反模式表现后果
为拆而拆单团队硬上微前端复杂度陡增,收益为负
共享业务状态各微应用互相读 store耦合回到单体
无沙箱直接组合全局变量互相覆盖随机崩溃、难排查
每应用一套框架React/Vue/Angular 混用体积翻倍、体验割裂
主应用强依赖远程远程挂了整站白屏可用性下降
契约口头约定无版本、无校验上线即事故

6.2 必须接受的代价

微前端不是免费的,要有清醒认识:

  • 包体积:框架重复加载风险,需靠 shared 与依赖治理控制;
  • 性能:运行时加载远程模块引入额外网络往返,首屏变慢;
  • 调试:跨应用调用栈难追踪,需要统一的错误上报与 source map 管理;
  • 体验一致性:多团队产出,交互与视觉容易割裂,需设计系统兜底;
  • 复杂度:沙箱、路由、通信、版本管理都是自建成本。

6.3 可用性兜底

主应用必须对远程模块故障有兜底——远程加载失败时降级为提示而非白屏:

const OrderList = lazy(() =>
  import('orderUi/OrderList').catch(() => ({
    default: () => <ModuleFallback name="订单模块" />,
  }))
);

这与 高可用与容错设计 中"隔离故障、优雅降级"的原则一脉相承:一个模块挂掉不应拖垮整个页面。


7. 小结

维度要点
动机多团队独立开发、独立部署、渐进重构
集成运行时(Module Federation)优先;iframe 仅强隔离场景
隔离Proxy 沙箱管全局、CSS Modules/前缀管样式
路由主应用持有路由,微应用只管子路由
状态只共享用户/主题等极少量全局状态
部署远程入口带版本、清单可切、故障可降级
底线单团队别硬上;不共享业务状态;远程故障不白屏

一句话记住:微前端是用"运行时的组合复杂度"换取"开发与部署的自治"。它适合多团队、边界清晰、发布节奏分化的场景;一旦跨应用共享业务状态、或没有沙箱与契约治理,就会退化成"分布式的单体"——既有微服务的复杂度,又没有它的收益。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「架构」更多文章

  1. 韧性工程与错误预算:从 SLO 到故障演练
  2. 数据网格(Data Mesh):领域数据产品与去中心化治理
  3. C4 模型与架构文档化实践