微前端架构 (Micro Frontends)
什么是微前端?
可以理解为:微服务架构思想在前端领域的映射和应用。
它们的解决思路是完全一致的:化整为零,分而治之。
| 维度 | 微服务 (Backend) | 微前端 (Frontend) |
|---|---|---|
| 拆分对象 | 庞大的后端应用 | 庞大的前端 SPA 应用 |
| 拆分边界 | 业务领域 (用户/订单/库存) | 页面路由或功能模块 (订单页/设置页) |
| 独立性 | 独立部署、独立数据库 | 独立部署、独立域名/路径 |
| 技术栈 | Java, Go, Node.js 混用 | React, Vue, Angular 混用 |
| 聚合方式 | API 网关 / RPC | 基座应用 (Container App) |
为什么要使用微前端?
假如我们的 Vine(仓库作业移动端)随着业务发展变得极其庞大,可能会面临以下痛点:
- 技术栈升级难:想用 React 19,但老代码依赖 React 16,升级成本巨大。
- 构建慢:改一行代码,整个项目构建需要 10 分钟。
- 维护难:代码耦合严重,“牵一发而动全身”。
微前端通过拆分应用,可以带来以下收益:
- 技术栈升级:子应用未来可以使用不同的技术栈(如 Vue3 或 React19),而不影响主应用。
- 独立构建:子应用独立构建,无需等待整个 Vine 项目构建。
- 独立部署:
- 发布解耦:子应用可独立上线/回滚,不受基座或其他模块发布节奏影响。
- 风险隔离:单个子应用变更出问题,不会阻塞或影响其他业务模块。
- 效率提升:构建、测试、部署范围更小,速度更快,迭代频率更高。
- 团队自治:不同团队可按自身节奏推进,减少跨团队协作成本。
- 灰度与实验更灵活:可针对单个子应用做灰度/AB,不牵动全站。
常见方案
qiankun (阿里)
- 基于 single-spa 封装
- 特点:HTML Entry 接入方式,样式隔离(Shadow DOM/Scope CSS),JS 沙箱
- 适用:传统 SPA 项目改造
MicroApp (京东)
- 基于 Web Component
- 特点:使用
<micro-app>标签接入,侵入性低
实战演练:Vine 成品完工微前端拆分
Vine 项目目前的架构为 模块化单体,所有业务模块(入库、出库、生产、用户)都在同一个代码仓库中。随着业务发展,成品完工 逐渐呈现出以下特征:
- 业务独立性强:成品完工(质检、完工入库、标签打印)与入库、出库模块耦合度低。
- 复杂度高:包含大量表单、扫描逻辑、离线兜底与任务状态流转,构建时间日益增长。
- 团队边界清晰:完工链路由独立小组维护,发布节奏与其他模块不同步。
假设基于以上原因,我们将 成品完工 拆分为独立微应用 vine-product。
https://github.com/1px-club/vine/commit/eec21270de2003870bacfd65abcc1840879169ae
https://github.com/1px-club/vine-product/commit/b6080b347c7e58013661ccfc2c89b6cb36cb8106
架构:Vine + vine-product(qiankun)
下面用“从浏览器发起请求”到“子应用被挂载运行”的顺序,把当前落地方案画成尽量通俗的架构图。
1) 访问与反向代理(域名 → Traefik → 各 Vite Dev Server)
开发环境里,多个前端应用都跑在不同的容器里(内部端口一致也没关系),统一由 Traefik 按 Host 规则分发。
┌──────────────────────────┐
│ Browser (macOS) │
│ 访问 http://vine.test/ │
└─────────────┬────────────┘
│
│ /etc/hosts: *.test -> 127.0.0.1
▼
┌──────────────────────────┐
│ Traefik (localhost:80) │
│ Docker provider │
│ 路由规则(Host + Path) │
└───────┬───────────┬──────┘
│ │
│ Host: vine.test && /product 相关资源
▼
┌────────────────────────────────────────────┐
│ 同域名(vine.test)下的两类流量 │
│ 1) 业务路由(例如 /product/finish)→ vine │
│ 2) 子应用资源(例如 /product/ 与 assets)→ │
│ vine-product(静态资源服务 / Vite Dev) │
└────────────────────────────────────────────┘2) 运行时挂载(qiankun 的 HTML Entry 模式)
基座应用在路由命中 product/finish 时加载子应用。子应用不是“打包进基座”,而是基座在运行时去拉取子应用的入口 HTML,然后执行里面的资源(在浏览器运行时(runtime)由主应用去请求子应用的静态资源,然后执行)。
┌───────────────────────────────────────────────────────────────┐
│ Vine 基座(http://vine.test) │
│ 路由:/product/finish │
└───────────────┬───────────────────────────────────────────────┘
│ loadMicroApp({
│ name: 'vine-product',
│ entry: VITE_VINE_PRODUCT_APP_URL,
│ container: <div>…</div>,
│ props: { baseRoute: '/product' }
│ })
▼
┌───────────────────────────────────────────────────────────────┐
│ qiankun Runtime │
│ 1. 请求 entry(子应用 index.html) │
│ 2. 解析 HTML 中的 <script>/<link>/<style> 资源 │
│ 3. 加载并执行子应用脚本(在 sandbox 约束下) │
│ 4. 调用子应用的 mount(props) │
└───────────────┬───────────────────────────────────────────────┘
▼
┌───────────────────────────────────────────────────────────────┐
│ Vine Product 子应用(/product/ 资源入口) │
│ - 在 mount(props) 里: │
│ - 找到 container 内的 #root │
│ - createApp(App).mount(rootEl) │
│ - 子应用路由 /finish(基座地址栏为 /product/finish) │
└───────────────────────────────────────────────────────────────┘部署最佳实践:同域名 + 子路径(推荐)
在 qiankun 的 HTML Entry 模式下,子应用需要被浏览器“请求到”(index.html + 资源文件),所以线上子应用一定要有可访问的静态资源服务;但这个访问入口不一定要暴露成独立域名。
更常见的落地方式是把子应用挂到主站同域名下的子路径里,例如把 vine-product 挂到 https://vine.1px.club/product/:
- 对基座(vine)而言:子应用 entry 使用同域名路径即可,例如
/product/ - 对子应用(vine-product)而言:构建时需要设置资源 base 为
/product/,确保脚本/样式等资源路径正确 - 对网关(Traefik/Nginx)而言:把同域名下的
/product路由转发到子应用静态服务(常见做法是子应用独立静态服务 + PathPrefix 路由)
当前落地:业务路由归基座,子应用只提供资源入口
目标是:用户视角里 vine-product 只是 Vine 的一个业务模块,必须通过基座进入;但工程与发布上仍保持 vine-product 独立构建、独立发布/回滚。
关键点是把“业务路由”和“子应用资源”拆开处理:
- 业务路由永远由基座承接:例如
GET /product/finish必须返回vine的index.html,保证刷新/后退不会“切壳”。 - 子应用只暴露资源入口:基座通过 qiankun 的 HTML Entry 去请求
GET /product/(子应用 entry)并加载脚本/样式等资源。 - 网关只转发必要路径到子应用服务:
- 生产:
/product/、/product/assets/*→vine-product;其他/product/*→vine - 开发:额外需要
/product/@vite/*、/product/src/*(Vite HMR 与源码模块)
- 生产:
- 子应用禁止独立打开:若浏览器直接打开子应用入口(例如
/product/),子应用运行时应重定向回基座路由(例如/product/finish),避免 standalone 形态对用户可见。
路由协作问题与处理(实践记录)
在基座 + 子应用的路由协作中,最容易踩的坑是“地址栏路由由基座控制,但子应用仍在监听并尝试解析”。典型表现如下:
- 告警:子应用在基座页面内仍然响应
/me、/workbench等主应用路由,导致No match for location。 - 交互失效:主应用 Tab 点击后,地址栏变化但子应用无有效路由匹配,用户感知为“无反应”。
解决思路(推荐):
- 基座接管全局路由:Tab 由基座统一处理,子应用仅负责
/product/finish等业务内路由。 - 子应用在基座模式使用内存路由:避免读取地址栏并解析主应用路由,从根源消除告警与无效匹配。
这个策略可以保证:
- 主应用路由与子应用路由职责清晰、互不干扰
- 子应用仍可独立构建/部署,但运行时由基座统一导航
这种方式可以同时满足:
- 独立部署:
vine-product产物更新后,基座无需重新构建即可生效 - 体验一致:刷新/后退不再跳出基座壳,避免布局/宽度/Router basename 等问题
访问链路梳理
场景一:通过基座访问业务路由(例如 /product/finish)
- 浏览器请求
https://vine.1px.club/product/finish - Traefik 网关匹配到
vine-product-business路由(priority=200),转发到 vine 容器 - Vine 的 Nginx 返回
index.html(通过try_files $uri $uri/ /index.html) - 浏览器加载 Vine 主应用,命中前端路由
/product/finish - 路由组件渲染"子应用容器",调用
loadMicroApp - qiankun 根据 entry 配置请求
https://vine.1px.club/product/ - Traefik 匹配到
vine-product路由(priority=100),转发到 vine-product 容器 - vine-product 的 Nginx 返回子应用的
index.html - qiankun 解析 HTML,加载并执行子应用的 JS/CSS 资源
- 子应用调用
mount生命周期,挂载到基座提供的 DOM 容器中
场景二:直接访问子应用入口(例如 /product/)
- 浏览器请求
https://vine.1px.club/product/ - Traefik 匹配到
vine-product路由,转发到 vine-product 容器 - vine-product 的 Nginx 返回
index.html - 浏览器加载执行子应用 JS
- 子应用检测到
window.__POWERED_BY_QIANKUN__为false(standalone 模式) - 执行
window.location.replace('/product/finish')重定向到基座 - 回到场景一的流程
关键点:
- 业务路由(
/product/finish等)由 Traefik 路由到 vine 容器,返回基座的 index.html - 资源路径(
/product/、/product/assets/*)由 Traefik 路由到 vine-product 容器 - vine 的 Nginx 不再处理
/product/*路径,避免路由冲突和重定向死循环
qiankun vs iframe:有什么区别?
很多人第一次接触"运行时加载子应用"会自然联想到 iframe。这个类比在"感受上"是对的:都是把另一个应用嵌进来;但在"实现上"差别很大。
1) 相同点(为什么像)
- 都能做到:子应用独立构建/独立部署,基座按需加载
- 都能做到:主应用提供容器,子应用负责自己 UI 与交互
- 都需要处理:通信、路由协作、样式隔离、权限与安全边界
2) 不同点(本质差异)
| 维度 | iframe | qiankun(HTML Entry) |
|---|---|---|
| 运行环境 | 独立浏览器上下文(独立 window/document) | 同一页面内执行(共享 window/document,靠沙箱做隔离) |
| DOM 归属 | 子应用 DOM 在 iframe 的 document 内 | 子应用 DOM 挂在基座给的 container 下(同一棵 DOM 树) |
| 样式隔离 | 天然隔离(iframe 内外互不影响) | 需要工程化隔离(如 Scope CSS/Shadow DOM 等策略) |
| 路由体验 | iframe 内路由与地址栏天然分离,常需额外同步 | 可用 basename 直接纳入同一地址栏路由体系 |
| 通信方式 | postMessage 为主(跨域也可) | props/全局状态/事件为主(更像模块协作) |
| 性能与开销 | 更强隔离但更重(多一层页面环境) | 更轻但需要治理(隔离与约束成本在工程侧) |
3) 什么时候选哪一个
- 更适合 iframe:
- 隔离优先(安全/样式/依赖必须完全隔离)
- 子应用与基座交互较少,更像“嵌入第三方系统”
- 更适合 qiankun:
- 需要像“同一个 App 的一个模块”一样融入基座(统一路由、统一登录态、统一 UI 壳)
- 需要更细粒度的主子通信与协同能力
什么是 JS 沙箱?
在 qiankun 这类“非 iframe 的微前端”里,子应用代码最终是在同一页面里执行的。JS 沙箱的目标是:尽量把子应用对全局环境的读写限制在一个可控范围内,避免子应用污染基座或其他子应用。
JS 沙箱更偏向工程层隔离(减少全局污染/副作用),不是浏览器提供的安全隔离。若需要更强的安全边界(隔离 DOM/存储/网络能力并做权限限制),通常需要使用 iframe 。
1) 沙箱要解决的典型问题
- 子应用修改全局变量:
window.xxx = ...,导致基座行为变化 - 子应用注册全局副作用:
addEventListener、定时器、全局单例等,卸载后残留 - 子应用改写原型/内置对象:例如改
Array.prototype,引发难以定位的兼容问题
2) 常见实现思路(通俗版)
Proxy 沙箱(现代浏览器常用)
- 给子应用提供一个“代理 window”
- 子应用写全局变量时写进代理对象里
- 子应用卸载时把这些变更统一清理
- 优点:兼容性与性能通常较好;缺点:并非绝对隔离,仍需约束副作用
快照沙箱(兼容旧环境的一种策略)
- 挂载前记录一份 window 状态快照
- 卸载时对比并回滚变更
- 优点:思路直观;缺点:开销更大、对复杂对象/副作用治理有限