Skip to content

微前端架构 (Micro Frontends)

什么是微前端?

可以理解为:微服务架构思想在前端领域的映射和应用。

它们的解决思路是完全一致的:化整为零,分而治之

维度微服务 (Backend)微前端 (Frontend)
拆分对象庞大的后端应用庞大的前端 SPA 应用
拆分边界业务领域 (用户/订单/库存)页面路由或功能模块 (订单页/设置页)
独立性独立部署、独立数据库独立部署、独立域名/路径
技术栈Java, Go, Node.js 混用React, Vue, Angular 混用
聚合方式API 网关 / RPC基座应用 (Container App)

为什么要使用微前端?

假如我们的 Vine(仓库作业移动端)随着业务发展变得极其庞大,可能会面临以下痛点:

  1. 技术栈升级难:想用 React 19,但老代码依赖 React 16,升级成本巨大。
  2. 构建慢:改一行代码,整个项目构建需要 10 分钟。
  3. 维护难:代码耦合严重,“牵一发而动全身”。

微前端通过拆分应用,可以带来以下收益:

  1. 技术栈升级:子应用未来可以使用不同的技术栈(如 Vue3 或 React19),而不影响主应用。
  2. 独立构建:子应用独立构建,无需等待整个 Vine 项目构建。
  3. 独立部署
    • 发布解耦:子应用可独立上线/回滚,不受基座或其他模块发布节奏影响。
    • 风险隔离:单个子应用变更出问题,不会阻塞或影响其他业务模块。
    • 效率提升:构建、测试、部署范围更小,速度更快,迭代频率更高。
    • 团队自治:不同团队可按自身节奏推进,减少跨团队协作成本。
    • 灰度与实验更灵活:可针对单个子应用做灰度/AB,不牵动全站。

常见方案

  1. qiankun (阿里)

    • 基于 single-spa 封装
    • 特点:HTML Entry 接入方式,样式隔离(Shadow DOM/Scope CSS),JS 沙箱
    • 适用:传统 SPA 项目改造
  2. 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 必须返回 vineindex.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

  1. 浏览器请求 https://vine.1px.club/product/finish
  2. Traefik 网关匹配到 vine-product-business 路由(priority=200),转发到 vine 容器
  3. Vine 的 Nginx 返回 index.html(通过 try_files $uri $uri/ /index.html
  4. 浏览器加载 Vine 主应用,命中前端路由 /product/finish
  5. 路由组件渲染"子应用容器",调用 loadMicroApp
  6. qiankun 根据 entry 配置请求 https://vine.1px.club/product/
  7. Traefik 匹配到 vine-product 路由(priority=100),转发到 vine-product 容器
  8. vine-product 的 Nginx 返回子应用的 index.html
  9. qiankun 解析 HTML,加载并执行子应用的 JS/CSS 资源
  10. 子应用调用 mount 生命周期,挂载到基座提供的 DOM 容器中

场景二:直接访问子应用入口(例如 /product/

  1. 浏览器请求 https://vine.1px.club/product/
  2. Traefik 匹配到 vine-product 路由,转发到 vine-product 容器
  3. vine-product 的 Nginx 返回 index.html
  4. 浏览器加载执行子应用 JS
  5. 子应用检测到 window.__POWERED_BY_QIANKUN__false(standalone 模式)
  6. 执行 window.location.replace('/product/finish') 重定向到基座
  7. 回到场景一的流程

关键点:

  • 业务路由(/product/finish 等)由 Traefik 路由到 vine 容器,返回基座的 index.html
  • 资源路径(/product//product/assets/*)由 Traefik 路由到 vine-product 容器
  • vine 的 Nginx 不再处理 /product/* 路径,避免路由冲突和重定向死循环

qiankun vs iframe:有什么区别?

很多人第一次接触"运行时加载子应用"会自然联想到 iframe。这个类比在"感受上"是对的:都是把另一个应用嵌进来;但在"实现上"差别很大。

1) 相同点(为什么像)

  • 都能做到:子应用独立构建/独立部署,基座按需加载
  • 都能做到:主应用提供容器,子应用负责自己 UI 与交互
  • 都需要处理:通信、路由协作、样式隔离、权限与安全边界

2) 不同点(本质差异)

维度iframeqiankun(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 状态快照
    • 卸载时对比并回滚变更
    • 优点:思路直观;缺点:开销更大、对复杂对象/副作用治理有限

Released under the MIT License.