Appearance
第16章 微前端的 DevOps 与工程化
"微前端的真正挑战不在拆分代码——在于让十个团队同时向生产环境交付,且互不干扰。"
本章要点
- 设计独立构建、独立部署的 CI/CD 管线,实现子应用从提交到上线的全自动化流水线
- 理解语义化版本在微前端中的特殊挑战,掌握兼容性矩阵与版本协商机制
- 构建跨应用的监控与可观测性体系,快速定位"到底是谁的子应用出了问题"
- 在微前端架构下实现灰度发布与 A/B 测试,做到子应用级别的精细化流量控制
凌晨两点四十五分,你被一条告警惊醒。
生产环境的错误率在过去十分钟内飙升了 300%。你打开 Grafana 面板,看到订单子应用的 JS 报错量从每分钟 3 次跳到了每分钟 120 次。你的第一反应是回滚——但回滚哪个?主应用半小时前刚部署了一个导航栏优化,商品子应用两小时前推了一版新的详情页,而订单子应用本身三天没有发布过。
错误堆栈指向一个 TypeError: Cannot read properties of undefined (reading 'formatPrice'),发生在订单子应用调用共享组件库的 @shared/utils 包中。你翻看提交记录,发现商品子应用两小时前的部署附带升级了共享组件库的版本,将 formatPrice 的函数签名从 formatPrice(value: number) 改成了 formatPrice(value: number, options?: FormatOptions)——本身是向后兼容的改动。但问题在于,Module Federation 的运行时版本协商将订单子应用拉到了新版本的共享库,而这个新版本内部重构了模块导出结构,formatPrice 从默认导出变成了具名导出。
三个子应用、三次独立部署、一个共享依赖、一次无意的破坏性变更——这就是微前端 DevOps 的真实战场。
这一章,我们不谈理论模型。我们谈的是:如何设计一套工程化体系,让上面这种事故不可能发生——或者至少,当它发生时,你能在 30 秒内定位原因、60 秒内完成回滚。
下图展示了微前端 CI/CD 管线的完整架构,从代码提交到生产部署:
16.1 独立构建 + 独立部署的 CI/CD 管线设计
微前端的核心承诺之一是独立部署。但"独立部署"远不是"每个子应用一个 Git 仓库、各跑各的 CI"这么简单。独立部署的真正挑战在于:如何在保证独立性的同时,维护全局一致性。
16.1.1 仓库策略:Monorepo vs Polyrepo
在设计 CI/CD 之前,必须先回答一个前置问题:代码怎么组织?
方案一:Polyrepo(多仓库)
├── repo: main-app # 主应用
├── repo: order-app # 订单子应用
├── repo: product-app # 商品子应用
├── repo: user-app # 用户子应用
└── repo: shared-libs # 共享库
方案二:Monorepo(单仓库)
repo: micro-frontend-platform
├── apps/
│ ├── main/ # 主应用
│ ├── order/ # 订单子应用
│ ├── product/ # 商品子应用
│ └── user/ # 用户子应用
├── packages/
│ ├── shared-utils/ # 共享工具库
│ ├── shared-components/ # 共享组件库
│ └── shared-types/ # 共享类型定义
└── turbo.json / nx.json # 构建编排两种策略各有利弊,但在实践中,Monorepo + 独立部署管线正在成为微前端团队的主流选择,原因有三:
- 原子性变更:修改共享库和使用方可以在同一个 PR 中完成,CI 自动验证兼容性
- 统一工具链:ESLint、TypeScript、构建配置在顶层统一管理,避免各子应用配置漂移
- 依赖可见性:在 Monorepo 中,谁依赖了什么、哪个版本、有没有冲突——一目了然
关键在于:Monorepo 不等于 Monobuild。代码在一起管理,但构建和部署是独立的。
下图对比了 Polyrepo 和 Monorepo 两种仓库策略在微前端场景下的工作流差异:
16.1.2 基于变更检测的增量构建
Monorepo 下的核心问题是:订单子应用改了一行代码,不应该触发商品子应用的构建。这需要变更检测。
yaml
# .github/workflows/ci.yml — GitHub Actions 实现
name: Micro Frontend CI/CD
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
main-app: ${{ steps.changes.outputs.main-app }}
order-app: ${{ steps.changes.outputs.order-app }}
product-app: ${{ steps.changes.outputs.product-app }}
user-app: ${{ steps.changes.outputs.user-app }}
shared-libs: ${{ steps.changes.outputs.shared-libs }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
main-app:
- 'apps/main/**'
- 'packages/shared-types/**'
order-app:
- 'apps/order/**'
- 'packages/shared-utils/**'
- 'packages/shared-components/**'
product-app:
- 'apps/product/**'
- 'packages/shared-utils/**'
- 'packages/shared-components/**'
user-app:
- 'apps/user/**'
- 'packages/shared-utils/**'
shared-libs:
- 'packages/**'
build-order-app:
needs: detect-changes
if: needs.detect-changes.outputs.order-app == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm --filter order-app build
- run: pnpm --filter order-app test
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: order-app-dist
path: apps/order/dist/
retention-days: 7
# build-product-app, build-user-app 结构类似,此处省略
deploy-order-app:
needs: [build-order-app]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: order-app-dist
path: dist/
- name: Deploy to CDN
run: |
# 带版本号的部署路径,支持回滚
VERSION=$(cat dist/version.json | jq -r '.version')
DEPLOY_PATH="micro-apps/order/${VERSION}"
aws s3 sync dist/ "s3://${CDN_BUCKET}/${DEPLOY_PATH}" \
--cache-control "public, max-age=31536000, immutable"
# 更新版本映射表(关键!)
echo "{\"version\": \"${VERSION}\", \"path\": \"${DEPLOY_PATH}\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
> /tmp/manifest.json
aws s3 cp /tmp/manifest.json \
"s3://${CDN_BUCKET}/micro-apps/order/latest.json" \
--cache-control "no-cache, no-store, must-revalidate"
env:
CDN_BUCKET: ${{ secrets.CDN_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}