微前端进阶(四)
子应用独立构建、多环境部署、灰度发布与版本管理全攻略
目录
- [微前端 CI/CD 设计原则](#微前端 CI/CD 设计原则)
- 子应用独立构建流水线
- 多环境部署策略
- 版本管理与依赖兼容性
- 灰度发布方案
- 子应用注册中心动态配置
- 监控与回滚机制
- 完整流水线实战
一、微前端 CI/CD 设计原则
1.1 核心原则
┌─────────────────────────────────────────────────────────────┐
│ 微前端 CI/CD 核心原则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 独立构建、独立部署 │
│ 每个子应用有自己的 CI/CD 流水线,互不阻塞 │
│ │
│ 2. 版本化发布 │
│ 每个子应用构建产物带版本号,基座按版本加载 │
│ │
│ 3. 向后兼容 │
│ 子应用的新版本必须兼容旧版基座提供的 API │
│ │
│ 4. 灰度可控 │
│ 支持按用户、按流量比例逐步放量 │
│ │
│ 5. 可观测 │
│ 发布过程有完整的监控、日志、告警 │
│ │
│ 6. 快速回滚 │
│ 发现问题能在 5 分钟内回滚到上一版本 │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 发布流程总览
开发分支 (develop)
│
┌─────▼─────┐
│ PR/MR │
└─────┬─────┘
│ Code Review + CI 检查
│
┌─────▼─────┐
│ 合并到 main │
└─────┬─────┘
│
┌─────▼─────┐
│ 构建产物 │
│ versioned │ → 推送至 CDN/OSS
└─────┬─────┘
│
┌─────▼─────┐
│ 测试环境 │ → 自动部署
└─────┬─────┘
│ 集成测试通过
│
┌─────▼─────┐
│ 预发布环境 │ → 自动部署 + E2E
└─────┬─────┘
│ 验收通过
│
┌─────▼─────┐
│ 灰度发布 │ → 10% → 50% → 100%
└─────┬─────┘
│ 监控指标正常
│
┌─────▼─────┐
│ 全量发布 │
│ 所有用户 │
└───────────┘
二、子应用独立构建流水线
2.1 构建产物规范
子应用构建产物结构:
dist/
├── version.json # 版本信息
├── app-react@1.2.3/ # 版本化目录
│ ├── index.html # 子应用入口 HTML
│ ├── js/
│ │ ├── app.abc123.js # 主 JS bundle
│ │ ├── vendor.def456.js # vendor bundle
│ │ └── async.ghi789.js # 异步 chunk
│ ├── css/
│ │ └── style.xyz789.css # 样式文件
│ └── assets/ # 静态资源
├── app-react@1.2.4/
│ └── ...
├── latest -> app-react@1.2.4 # 最新版本软链接
└── stable -> app-react@1.2.3 # 稳定版本软链接
json
// dist/version.json
{
"name": "app-react",
"version": "1.2.3",
"buildTime": "2024-01-15T10:30:00Z",
"commitHash": "a1b2c3d4e5f6",
"branch": "main",
"ciBuildId": "12345",
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0",
"@company/ui": "2.1.0"
},
"compatiblePortal": ">=2.0.0 <3.0.0",
"changelog": [
"feat: 新增数据导出功能",
"fix: 修复列表页分页问题"
]
}
2.2 构建脚本
json
// apps/app-react/package.json
{
"name": "app-react",
"version": "1.2.3",
"scripts": {
"dev": "vite --port 3001",
"build": "vite build",
"build:versioned": "node scripts/build-with-version.js",
"build:analyze": "vite build --analyze",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
}
}
javascript
// apps/app-react/scripts/build-with-version.js
// 带版本号的构建脚本
const { execSync } = require('child_process');
const { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } = require('fs');
const { join, resolve } = require('path');
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'));
const version = pkg.version;
const distDir = resolve(__dirname, '../dist');
const versionDir = join(distDir, `app-react@${version}`);
// 1. 运行标准构建
console.log(`[Build] 构建 app-react@${version}...`);
execSync('vite build', { stdio: 'inherit' });
// 2. 创建版本化目录
console.log(`[Build] 创建版本目录: app-react@${version}`);
if (!existsSync(versionDir)) {
mkdirSync(versionDir, { recursive: true });
}
// 3. 复制构建产物到版本目录
execSync(`xcopy /E /I /Y ${join(distDir, '*')} ${versionDir}`, { stdio: 'inherit' });
// 4. 更新 latest 软链接(在 OSS 上操作)
console.log(`[Build] 构建完成: app-react@${version}`);
2.3 GitLab CI 流水线
yaml
# apps/app-react/.gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
- version
variables:
APP_NAME: app-react
DOCKER_IMAGE: $CI_REGISTRY_IMAGE/$APP_NAME
CDN_BUCKET: micro-frontend-prod
CDN_PREFIX: apps/$APP_NAME
cache:
key: ${CI_COMMIT_REF_SLUG}-$APP_NAME
paths:
- node_modules/
- .vite-cache/
# ======== Lint ========
lint:
stage: lint
script:
- pnpm install --frozen-lockfile
- pnpm lint
- pnpm typecheck
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == 'main'
# ======== 单元测试 ========
unit-test:
stage: test
script:
- pnpm install --frozen-lockfile
- pnpm test:unit --coverage
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == 'main'
# ======== 构建 ========
build:
stage: build
script:
- pnpm install --frozen-lockfile
- pnpm build:versioned
artifacts:
paths:
- dist/
expire_in: 1 week
rules:
- if: $CI_COMMIT_BRANCH == 'main'
# ======== 部署到测试环境 ========
deploy:staging:
stage: deploy
script:
# 上传到 CDN(测试环境)
- aws s3 sync dist/ s3://$CDN_BUCKET-staging/$CDN_PREFIX/ --delete
# 刷新 CDN 缓存
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/$CDN_PREFIX/*"
environment:
name: staging
url: https://staging.portal.company.com/$CDN_PREFIX
rules:
- if: $CI_COMMIT_BRANCH == 'main'
# ======== 部署到预发布环境 ========
deploy:preprod:
stage: deploy
script:
- aws s3 sync dist/ s3://$CDN_BUCKET-preprod/$CDN_PREFIX/ --delete
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/$CDN_PREFIX/*"
environment:
name: preprod
url: https://preprod.portal.company.com/$CDN_PREFIX
rules:
- if: $CI_COMMIT_BRANCH == 'main'
- when: manual # 手动触发
# ======== 创建版本 Tag ========
version-tag:
stage: version
script:
- VERSION=$(node -p "require('./package.json').version")
- git tag "$APP_NAME-v$VERSION" $CI_COMMIT_SHA
- git push origin "$APP_NAME-v$VERSION"
rules:
- if: $CI_COMMIT_BRANCH == 'main'
- when: manual
2.4 多子应用并行构建
yaml
# .gitlab-ci.yml(平台级,用于并行构建所有子应用)
stages:
- build-all
- deploy-all
variables:
SUB_APPS: "portal app-react app-vue app-angular"
build-all:
stage: build-all
trigger:
strategy: depend
include:
- local: apps/portal/.gitlab-ci.yml
- local: apps/app-react/.gitlab-ci.yml
- local: apps/app-vue/.gitlab-ci.yml
- local: apps/app-angular/.gitlab-ci.yml
deploy-all:
stage: deploy-all
needs:
- job: build-all
artifacts: false
script:
- echo "所有子应用构建完成,触发全局部署"
三、多环境部署策略
3.1 环境划分
| 环境 | 用途 | 部署方式 | 数据源 | 监控级别 | 访问限制 |
|---|---|---|---|---|---|
| dev | 本地开发 | 本地启动 | Mock | 无 | 开发者 |
| staging | 集成测试 | 自动部署 | 测试库 | 基础 | 内网 |
| preprod | 预发布验收 | 手动触发 | 脱敏数据 | 完整 | 项目组 |
| canary | 灰度验证 | 自动灰度 | 生产库 | 完整+详细 | 白名单用户 |
| production | 生产 | 灰度→全量 | 生产库 | 完整+告警 | 所有用户 |
3.2 环境配置管理
typescript
// config/environments.ts
interface EnvironmentConfig {
name: string;
apiBase: string;
authUrl: string;
cdnBase: string;
logLevel: 'debug' | 'info' | 'warn' | 'error';
enableMonitor: boolean;
enableSandbox: boolean;
}
const ENVIRONMENTS: Record<string, EnvironmentConfig> = {
dev: {
name: 'development',
apiBase: 'http://localhost:9090/api',
authUrl: 'http://localhost:9090/auth',
cdnBase: 'http://localhost:8080',
logLevel: 'debug',
enableMonitor: false,
enableSandbox: true
},
staging: {
name: 'staging',
apiBase: 'https://api-staging.company.com',
authUrl: 'https://sso-staging.company.com/auth',
cdnBase: 'https://cdn-staging.company.com',
logLevel: 'info',
enableMonitor: true,
enableSandbox: true
},
preprod: {
name: 'preprod',
apiBase: 'https://api-preprod.company.com',
authUrl: 'https://sso.company.com/auth',
cdnBase: 'https://cdn-preprod.company.com',
logLevel: 'info',
enableMonitor: true,
enableSandbox: true
},
production: {
name: 'production',
apiBase: 'https://api.company.com',
authUrl: 'https://sso.company.com/auth',
cdnBase: 'https://cdn.company.com',
logLevel: 'warn',
enableMonitor: true,
enableSandbox: true
}
};
// 根据部署环境自动选择配置
export function getEnvironmentConfig(): EnvironmentConfig {
const env = import.meta.env.VITE_APP_ENV || 'dev';
return ENVIRONMENTS[env] || ENVIRONMENTS.dev;
}
3.3 Docker 多环境部署
dockerfile
# Dockerfile(多阶段构建)
# ======== 构建阶段 ========
FROM node:20-alpine AS builder
ARG APP_ENV=production
ENV VITE_APP_ENV=${APP_ENV}
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# ======== Nginx 运行阶段 ========
FROM nginx:stable-alpine
ARG APP_ENV=production
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx/${APP_ENV}.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
bash
# docker-compose 环境部署
# docker-compose.staging.yml
version: '3.8'
services:
portal:
build:
context: ./apps/portal
args:
APP_ENV: staging
ports:
- "8080:80"
environment:
- VITE_APP_ENV=staging
app-react:
build:
context: ./apps/app-react
args:
APP_ENV: staging
ports:
- "3001:80"
app-vue:
build:
context: ./apps/app-vue
args:
APP_ENV: staging
ports:
- "3002:80"
3.4 CDN 版本化部署
typescript
// scripts/deploy-to-cdn.ts
// 将构建产物部署到 CDN,实现多版本共存
interface DeployOptions {
appName: string;
version: string;
env: 'staging' | 'preprod' | 'production';
sourceDir: string;
}
async function deployToCDN(options: DeployOptions) {
const { appName, version, env, sourceDir } = options;
const cdnPrefix = `apps/${appName}`;
const versionPath = `${cdnPrefix}/${version}`;
const latestPath = `${cdnPrefix}/latest`;
console.log(`[Deploy] 部署 ${appName}@${version} 到 ${env} 环境`);
// 1. 上传版本化目录
await s3Upload({
source: sourceDir,
target: versionPath,
bucket: getBucketName(env)
});
// 2. 更新 latest 指向
await s3Copy({
source: versionPath,
target: latestPath,
bucket: getBucketName(env)
});
// 3. 记录部署历史
await recordDeployment({
appName,
version,
env,
deployedAt: new Date().toISOString(),
deployedBy: process.env.CI_COMMIT_AUTHOR
});
// 4. 通知平台管理中心
await notifyPortalCenter({
appName,
version,
env,
entryUrl: `/${versionPath}/index.html`
});
console.log(`[Deploy] ${appName}@${version} 部署完成`);
}
四、版本管理与依赖兼容性
4.1 版本规范
┌─────────────────────────────────────────────────────────────┐
│ 版本号规范(SemVer) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 格式: MAJOR.MINOR.PATCH │
│ │
│ 示例: app-react@2.3.1 │
│ ↑ ↑ ↑ ↑ │
│ 应用名 主版本 次版本 补丁 │
│ │
│ MAJOR: 不兼容的 API 变更 │
│ MINOR: 向下兼容的功能新增 │
│ PATCH: 向下兼容的问题修复 │
│ │
│ Tag 格式: {app-name}-v{version} │
│ 示例: app-react-v2.3.1 │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 依赖兼容性声明
typescript
// 子应用的 version.json 中的兼容性声明
interface VersionCompatibility {
// 此版本要求基座版本 >= 2.0.0 且 < 3.0.0
compatiblePortal: string;
// 此版本依赖的共享库版本
sharedDependencies: Record<string, string>;
// 此版本引入的 Breaking Changes
breakingChanges?: Array<{
description: string;
affectedApis: string[];
migrationGuide: string;
}>;
// 此版本的新特性
features?: string[];
}
// 基座的版本兼容性校验
function checkVersionCompatibility(
subAppVersion: string,
portalVersion: string
): { compatible: boolean; reason?: string } {
const semver = require('semver');
// 获取子应用的兼容性声明
const compatibility = getCompatibility(subAppVersion);
// 检查基座版本是否在兼容范围内
if (!semver.satisfies(portalVersion, compatibility.compatiblePortal)) {
return {
compatible: false,
reason: `子应用 ${subAppVersion} 需要基座版本 ${compatibility.compatiblePortal},当前基座版本 ${portalVersion}`
};
}
return { compatible: true };
}
4.3 版本管理服务
typescript
// services/version-manager.ts
// 版本管理 API 服务
interface AppVersion {
appName: string;
version: string;
buildTime: string;
commitHash: string;
status: 'active' | 'gray' | 'rolled-back' | 'archived';
grayPercent?: number;
deployedAt: string;
deployedBy: string;
}
class VersionManager {
private versions: Map<string, AppVersion[]> = new Map();
// 注册新版本
async registerVersion(appName: string, version: string): Promise<void> {
const key = `${appName}@${version}`;
const record: AppVersion = {
appName,
version,
buildTime: new Date().toISOString(),
commitHash: process.env.CI_COMMIT_SHA || '',
status: 'active',
deployedAt: new Date().toISOString(),
deployedBy: process.env.CI_COMMIT_AUTHOR || 'unknown'
};
if (!this.versions.has(appName)) {
this.versions.set(appName, []);
}
this.versions.get(appName)!.push(record);
// 持久化到数据库
await db.collection('app-versions').insertOne(record);
}
// 获取当前活跃版本
async getActiveVersion(appName: string): Promise<AppVersion | null> {
const apps = this.versions.get(appName) || [];
return apps.find(v => v.status === 'active') || null;
}
// 获取灰度版本
async getGrayVersion(appName: string): Promise<AppVersion | null> {
const apps = this.versions.get(appName) || [];
return apps.find(v => v.status === 'gray') || null;
}
// 切换版本
async switchVersion(
appName: string,
version: string,
strategy: 'immediate' | 'gray' = 'immediate',
grayPercent?: number
): Promise<void> {
// 当前活跃版本标记为 archived
const current = await this.getActiveVersion(appName);
if (current) {
current.status = 'archived';
}
// 新版本激活
const target = this.versions.get(appName)
?.find(v => v.version === version);
if (target) {
target.status = strategy === 'gray' ? 'gray' : 'active';
if (strategy === 'gray') {
target.grayPercent = grayPercent;
}
}
// 通知基座更新路由
await this.notifyPortal(appName, version);
}
// 回滚版本
async rollback(appName: string, targetVersion: string): Promise<void> {
const current = await this.getActiveVersion(appName);
if (current) {
current.status = 'rolled-back';
}
await this.switchVersion(appName, targetVersion, 'immediate');
}
// 获取版本历史
async getVersionHistory(appName: string): Promise<AppVersion[]> {
return this.versions.get(appName) || [];
}
}
五、灰度发布方案
5.1 灰度策略设计
┌─────────────────────────────────────────────────────────────┐
│ 灰度发布策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 策略一:基于用户白名单 │
│ - 指定内部用户/测试账号先使用新版本 │
│ - 适用于功能验收阶段 │
│ │
│ 策略二:基于流量比例 │
│ - 10% 用户 → 50% 用户 → 100% 用户 │
│ - 适用于稳定性和性能验证 │
│ │
│ 策略三:基于用户属性 │
│ - 按地域、用户等级、设备类型等维度灰度 │
│ - 适用于针对性验证 │
│ │
│ 策略四:基于路由/功能 │
│ - 按 API 路由或功能模块灰度 │
│ - 适用于逐步上线新功能 │
│ │
└─────────────────────────────────────────────────────────────┘
5.2 灰度版本选择器
typescript
// src/micro/gray-selector.ts
// 在基座应用中根据灰度策略选择子应用版本
interface GrayConfig {
appName: string;
strategy: 'whitelist' | 'percentage' | 'attribute';
whitelist?: string[]; // 白名单用户 ID
percentage?: number; // 流量比例 0-100
attribute?: {
key: string; // 用户属性 key
values: string[]; // 匹配值
};
newVersion: string; // 灰度版本
stableVersion: string; // 稳定版本
}
class GraySelector {
private grayConfigs: Map<string, GrayConfig> = new Map();
// 更新灰度配置(从管理后台获取)
async updateGrayConfigs(): Promise<void> {
try {
const configs = await request.get<GrayConfig[]>('/api/gray/configs');
configs.forEach(cfg => {
this.grayConfigs.set(cfg.appName, cfg);
});
} catch {
console.warn('[Gray] 获取灰度配置失败,使用稳定版本');
}
}
// 为用户选择版本
selectVersion(appName: string, userId: string, userAttributes?: Record<string, string>): string {
const config = this.grayConfigs.get(appName);
if (!config) return 'latest'; // 没有灰度配置,使用最新版
switch (config.strategy) {
case 'whitelist':
return this.whitelistStrategy(config, userId);
case 'percentage':
return this.percentageStrategy(config, userId);
case 'attribute':
return this.attributeStrategy(config, userAttributes);
default:
return config.stableVersion;
}
}
private whitelistStrategy(config: GrayConfig, userId: string): string {
if (config.whitelist?.includes(userId)) {
console.log(`[Gray] 用户 ${userId} 在白名单中,使用新版 ${config.newVersion}`);
return config.newVersion;
}
return config.stableVersion;
}
private percentageStrategy(config: GrayConfig, userId: string): string {
// 根据用户 ID 的 hash 值决定是否命中灰度
const hash = this.hashUserId(userId);
const isGray = hash % 100 < (config.percentage || 0);
if (isGray) {
console.log(`[Gray] 用户 ${userId} 命中灰度 ${config.percentage}%,使用新版`);
return config.newVersion;
}
return config.stableVersion;
}
private attributeStrategy(config: GrayConfig, userAttributes?: Record<string, string>): string {
if (!config.attribute || !userAttributes) return config.stableVersion;
const userValue = userAttributes[config.attribute.key];
if (userValue && config.attribute.values.includes(userValue)) {
return config.newVersion;
}
return config.stableVersion;
}
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
const char = userId.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
}
export const graySelector = new GraySelector();
5.3 基座动态加载灰度版本
typescript
// src/micro/dynamic-loader.ts
// 基座根据灰度策略动态决定加载哪个版本的子应用
import { graySelector } from './gray-selector';
import { useUserStore } from '@/stores/user';
// 获取子应用的入口地址(带版本号)
export async function getMicroAppEntry(appName: string): Promise<string> {
const userStore = useUserStore();
const userId = userStore.userInfo?.id || 'anonymous';
// 灰度策略选择版本
const version = graySelector.selectVersion(
appName,
userId,
{ role: userStore.userInfo?.roles?.[0] }
);
// 构造版本化入口 URL
const cdnBase = getEnvironmentConfig().cdnBase;
return `${cdnBase}/apps/${appName}/${version}/index.html`;
}
// 动态注册子应用(带灰度版本)
export async function registerMicroAppWithGray(appName: string) {
const entry = await getMicroAppEntry(appName);
const activeRule = getActiveRule(appName);
registerMicroApps([
{
name: appName,
entry,
container: '#micro-app-container',
activeRule,
props: {
appName,
version: graySelector.selectVersion(appName, ''),
grayEnabled: true
}
}
]);
}
5.4 灰度管理后台 API
typescript
// services/gray-management.ts
// 灰度管理后台接口
interface GrayReleaseRequest {
appName: string;
newVersion: string;
strategy: GrayConfig['strategy'];
whitelist?: string[];
percentage?: number;
attribute?: GrayConfig['attribute'];
}
class GrayManagementService {
// 创建灰度发布
async createGrayRelease(req: GrayReleaseRequest): Promise<void> {
// 1. 校验版本号是否存在
await this.validateVersion(req.appName, req.newVersion);
// 2. 保存灰度配置
await db.collection('gray-configs').updateOne(
{ appName: req.appName },
{ $set: req },
{ upsert: true }
);
// 3. 触发基座刷新灰度配置
await this.notifyPortalRefresh(req.appName);
// 4. 记录审计日志
await this.auditLog('gray-create', req);
}
// 调整灰度比例
async adjustGrayPercentage(appName: string, newPercentage: number): Promise<void> {
// 渐进式调整:10% → 30% → 50% → 80% → 100%
const allowedSteps = [10, 30, 50, 80, 100];
if (!allowedSteps.includes(newPercentage)) {
throw new Error('灰度比例必须是 10/30/50/80/100');
}
await db.collection('gray-configs').updateOne(
{ appName },
{ $set: { percentage: newPercentage } }
);
await this.auditLog('gray-adjust', { appName, percentage: newPercentage });
}
// 结束灰度,全量发布
async finishGrayRelease(appName: string): Promise<void> {
const config = await db.collection('gray-configs').findOne({ appName });
// 新版转为 stable
await db.collection('app-versions').updateOne(
{ appName, version: config.newVersion },
{ $set: { status: 'active' } }
);
// 旧版标记为 archived
await db.collection('app-versions').updateOne(
{ appName, version: config.stableVersion },
{ $set: { status: 'archived' } }
);
// 清除灰度配置
await db.collection('gray-configs').deleteOne({ appName });
await this.auditLog('gray-finish', { appName });
}
// 回滚灰度
async rollbackGrayRelease(appName: string): Promise<void> {
const config = await db.collection('gray-configs').findOne({ appName });
// 新版标记为 rolled-back
await db.collection('app-versions').updateOne(
{ appName, version: config.newVersion },
{ $set: { status: 'rolled-back' } }
);
// 清除灰度配置
await db.collection('gray-configs').deleteOne({ appName });
// 通知基座使用稳定版本
await this.notifyPortalRollback(appName, config.stableVersion);
await this.auditLog('gray-rollback', { appName });
}
private async validateVersion(appName: string, version: string): Promise<void> {
const exists = await db.collection('app-versions').findOne({
appName, version
});
if (!exists) {
throw new Error(`版本 ${appName}@${version} 不存在`);
}
}
private async notifyPortalRefresh(appName: string): Promise<void> {
// 通过消息队列通知基座刷新配置
await messageQueue.publish('portal:gray-config-update', { appName });
}
private async auditLog(action: string, data: any): Promise<void> {
await db.collection('audit-logs').insertOne({
action,
data,
timestamp: new Date(),
operator: process.env.CI_COMMIT_AUTHOR || 'system'
});
}
}
六、子应用注册中心动态配置
6.1 动态配置服务
typescript
// src/micro/dynamic-config.ts
// 子应用注册中心:支持运行时动态更新配置
interface DynamicMicroConfig {
name: string;
entry: string;
activeRule: string;
version: string;
status: 'enabled' | 'disabled' | 'maintenance';
permissions?: string[];
features?: Record<string, boolean>;
}
class DynamicMicroConfigLoader {
private configs: Map<string, DynamicMicroConfig> = new Map();
private pollingTimer: ReturnType<typeof setInterval> | null = null;
// 启动定时轮询配置
startPolling(interval = 30000) {
this.pollingTimer = setInterval(() => {
this.refreshConfigs();
}, interval);
}
// 停止轮询
stopPolling() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
}
// 刷新配置
async refreshConfigs(): Promise<void> {
try {
const configs = await request.get<DynamicMicroConfig[]>('/api/micro-apps/configs');
configs.forEach(cfg => {
this.configs.set(cfg.name, cfg);
});
console.log(`[MicroConfig] 已更新 ${configs.length} 个子应用配置`);
} catch (err) {
console.warn('[MicroConfig] 刷新配置失败,使用缓存配置');
}
}
// 获取子应用配置
getConfig(appName: string): DynamicMicroConfig | undefined {
return this.configs.get(appName);
}
// 获取所有启用的子应用
getEnabledApps(): DynamicMicroConfig[] {
return Array.from(this.configs.values())
.filter(cfg => cfg.status === 'enabled');
}
// 检查子应用是否可用
isAppAvailable(appName: string): boolean {
const config = this.configs.get(appName);
return config?.status === 'enabled';
}
// 获取子应用特性开关
getFeature(appName: string, featureName: string): boolean {
const config = this.configs.get(appName);
return config?.features?.[featureName] ?? false;
}
}
export const microConfigLoader = new DynamicMicroConfigLoader();
6.2 管理后台配置界面
typescript
// 子应用配置管理 API
// 用于管理后台动态增删改子应用
// 注册新子应用
async function registerNewApp(config: {
name: string;
entry: string;
activeRule: string;
platform: string;
}) {
await request.post('/api/micro-apps/register', config);
// 触发基座热更新
await microConfigLoader.refreshConfigs();
}
// 启用/禁用子应用
async function toggleAppStatus(appName: string, enabled: boolean) {
await request.put(`/api/micro-apps/${appName}/status`, {
status: enabled ? 'enabled' : 'disabled'
});
await microConfigLoader.refreshConfigs();
}
// 更新子应用入口
async function updateAppEntry(appName: string, newEntry: string) {
await request.put(`/api/micro-apps/${appName}/entry`, {
entry: newEntry
});
await microConfigLoader.refreshConfigs();
}
// 设置特性开关
async function setFeatureFlag(
appName: string,
featureName: string,
enabled: boolean
) {
await request.put(`/api/micro-apps/${appName}/features`, {
[featureName]: enabled
});
await microConfigLoader.refreshConfigs();
}
七、监控与回滚机制
7.1 发布监控指标
typescript
// services/deployment-monitor.ts
// 发布期间的监控指标采集
interface DeploymentMetrics {
appName: string;
version: string;
deployTime: string;
// 错误率
errorRate: number; // 当前 vs 发布前 24h 基线
jsErrorRate: number;
apiErrorRate: number;
microAppLoadErrorRate: number;
// 性能指标
avgLoadTime: number; // 子应用平均加载时间
p95LoadTime: number;
avgSwitchTime: number; // 子应用切换时间
// 业务指标
activeUsers: number;
pageViews: number;
conversionRate?: number;
// 资源指标
cpuUsage: number;
memoryUsage: number;
cdnHitRate: number;
}
class DeploymentMonitor {
private baseline: Record<string, number> = {};
// 获取发布前 24h 基线
async loadBaseline(appName: string): Promise<void> {
const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;
const metrics = await this.queryMetrics(appName, twentyFourHoursAgo);
this.baseline = {
errorRate: this.average(metrics.map(m => m.errorRate)),
avgLoadTime: this.average(metrics.map(m => m.avgLoadTime)),
activeUsers: this.average(metrics.map(m => m.activeUsers))
};
}
// 检查当前指标是否在安全范围内
async checkHealth(appName: string): Promise<{
healthy: boolean;
alerts: string[];
}> {
const current = await this.getCurrentMetrics(appName);
const alerts: string[] = [];
// 错误率超过基线 2 倍
if (current.errorRate > this.baseline.errorRate * 2) {
alerts.push(`错误率异常: ${current.errorRate.toFixed(2)}% (基线: ${this.baseline.errorRate.toFixed(2)}%)`);
}
// JS 错误率 > 1%
if (current.jsErrorRate > 1) {
alerts.push(`JS 错误率过高: ${current.jsErrorRate.toFixed(2)}%`);
}
// 加载时间超过 3 秒
if (current.p95LoadTime > 3000) {
alerts.push(`P95 加载时间过长: ${current.p95LoadTime}ms`);
}
// 用户量骤降超过 50%
if (current.activeUsers < this.baseline.activeUsers * 0.5) {
alerts.push(`活跃用户骤降: ${current.activeUsers} (基线: ${this.baseline.activeUsers})`);
}
return {
healthy: alerts.length === 0,
alerts
};
}
// 自动回滚决策
async shouldAutoRollback(appName: string): Promise<boolean> {
const { healthy, alerts } = await this.checkHealth(appName);
if (!healthy) {
console.warn(`[AutoRollback] ${appName} 检测到异常:`, alerts);
// 连续 3 次检查都异常才自动回滚
const consecutiveFailures = await this.getConsecutiveFailures(appName);
if (consecutiveFailures >= 3) {
return true;
}
}
return false;
}
private async getCurrentMetrics(appName: string): Promise<DeploymentMetrics> {
// 从监控平台获取当前指标
return request.get(`/api/monitor/metrics/${appName}/current`);
}
private average(values: number[]): number {
return values.length > 0
? values.reduce((a, b) => a + b, 0) / values.length
: 0;
}
}
7.2 自动回滚机制
typescript
// services/auto-rollback.ts
import { DeploymentMonitor } from './deployment-monitor';
class AutoRollbackService {
private monitor: DeploymentMonitor;
private rollbackInProgress: Set<string> = new Set();
constructor() {
this.monitor = new DeploymentMonitor();
}
// 启动发布监控
async watchDeployment(appName: string, version: string) {
console.log(`[AutoRollback] 开始监控 ${appName}@${version} 发布`);
// 加载基线
await this.monitor.loadBaseline(appName);
// 每 30 秒检查一次,持续 30 分钟
const checkInterval = 30000;
const maxDuration = 30 * 60 * 1000;
let elapsed = 0;
const timer = setInterval(async () => {
elapsed += checkInterval;
// 检查是否需要自动回滚
if (await this.monitor.shouldAutoRollback(appName)) {
clearInterval(timer);
await this.executeAutoRollback(appName, version);
return;
}
// 超过监控时长,结束监控
if (elapsed >= maxDuration) {
clearInterval(timer);
console.log(`[AutoRollback] ${appName}@${version} 发布监控完成,一切正常`);
}
}, checkInterval);
}
private async executeAutoRollback(appName: string, badVersion: string) {
if (this.rollbackInProgress.has(appName)) {
console.warn(`[AutoRollback] ${appName} 回滚正在进行中,跳过`);
return;
}
this.rollbackInProgress.add(appName);
try {
// 获取上一个稳定版本
const stableVersion = await this.getPreviousStableVersion(appName);
console.warn(`[AutoRollback] 自动回滚 ${appName}: ${badVersion} → ${stableVersion}`);
// 执行回滚
await request.post('/api/gray/rollback', {
appName,
targetVersion: stableVersion,
reason: 'auto-rollback: 异常指标触发',
triggeredBy: 'system'
});
// 通知相关人员
await this.notifyTeam({
appName,
badVersion,
stableVersion,
reason: '发布后错误率超过阈值,已自动回滚'
});
} catch (err) {
console.error(`[AutoRollback] 回滚失败:`, err);
} finally {
this.rollbackInProgress.delete(appName);
}
}
private async getPreviousStableVersion(appName: string): Promise<string> {
const versions = await request.get<AppVersion[]>(
`/api/app-versions/${appName}/history`
);
const stable = versions.find(v =>
v.status === 'active' || v.status === 'rolled-back'
);
return stable?.version || 'latest';
}
}
7.3 发布审批流程
typescript
// services/deployment-approval.ts
// 发布审批工作流
interface DeploymentRequest {
id: string;
appName: string;
version: string;
environment: 'staging' | 'preprod' | 'production';
requester: string;
changelog: string[];
riskLevel: 'low' | 'medium' | 'high';
requiredApprovers: string[];
approvals: Array<{
user: string;
status: 'pending' | 'approved' | 'rejected';
comment?: string;
timestamp?: string;
}>;
status: 'pending' | 'approved' | 'rejected' | 'deployed' | 'rolled-back';
createdAt: string;
}
class DeploymentApprovalService {
// 创建发布请求
async createRequest(req: Omit<DeploymentRequest, 'id' | 'status' | 'createdAt'>): Promise<string> {
const request: DeploymentRequest = {
...req,
id: generateId(),
status: 'pending',
createdAt: new Date().toISOString()
};
// 根据风险等级决定审批人
if (req.riskLevel === 'high') {
request.requiredApprovers = [
...req.requiredApprovers,
'tech-lead',
'qa-lead'
];
}
await db.collection('deployment-requests').insertOne(request);
// 发送审批通知
await this.sendApprovalNotifications(request);
return request.id;
}
// 审批通过
async approve(requestId: string, user: string): Promise<void> {
const request = await db.collection('deployment-requests')
.findOne({ id: requestId });
// 更新审批状态
request.approvals.push({
user,
status: 'approved',
timestamp: new Date().toISOString()
});
// 检查是否所有必需的审批人已通过
const approvedUsers = request.approvals
.filter(a => a.status === 'approved')
.map(a => a.user);
const allApproved = request.requiredApprovers.every(
approver => approvedUsers.includes(approver)
);
if (allApproved) {
request.status = 'approved';
// 触发部署
await this.triggerDeployment(request);
}
await db.collection('deployment-requests').updateOne(
{ id: requestId },
{ $set: request }
);
}
}
八、完整流水线实战
8.1 一键部署脚本
bash
#!/bin/bash
# scripts/deploy.sh
# 一键部署脚本:构建 + 上传 CDN + 更新注册中心
set -e
APP_NAME=$1
ENV=${2:-staging}
if [ -z "$APP_NAME" ]; then
echo "用法: ./deploy.sh <app-name> [environment]"
echo "示例: ./deploy.sh app-react production"
exit 1
fi
echo "=========================================="
echo " 部署 $APP_NAME 到 $ENV 环境"
echo "=========================================="
# 1. 运行测试
echo "[Step 1/5] 运行测试..."
cd apps/$APP_NAME
pnpm lint
pnpm typecheck
pnpm test:unit
# 2. 构建
echo "[Step 2/5] 构建..."
pnpm build:versioned
# 3. 上传 CDN
echo "[Step 3/5] 上传 CDN..."
VERSION=$(node -p "require('./package.json').version")
BUCKET="micro-frontend-$ENV"
aws s3 sync dist/ s3://$BUCKET/apps/$APP_NAME/ --delete
# 4. 更新注册中心
echo "[Step 4/5] 更新注册中心..."
curl -X PUT "https://portal-api.company.com/api/micro-apps/$APP_NAME/version" \
-H "Content-Type: application/json" \
-d "{\"version\": \"$VERSION\"}"
# 5. 刷新 CDN 缓存
echo "[Step 5/5] 刷新 CDN 缓存..."
aws cloudfront create-invalidation \
--distribution-id $CLOUDFRONT_ID \
--paths "/apps/$APP_NAME/*"
echo "=========================================="
echo " $APP_NAME@$VERSION 已部署到 $ENV"
echo "=========================================="
8.2 GitHub Actions 完整工作流
yaml
# .github/workflows/deploy-production.yml
name: Deploy to Production
on:
workflow_dispatch:
inputs:
app:
description: '子应用名称'
required: true
type: choice
options:
- app-react
- app-vue
- app-angular
- portal
strategy:
description: '发布策略'
required: true
type: choice
default: 'gray'
options:
- gray
- immediate
env:
NODE_VERSION: 20
PNPM_VERSION: 8
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install dependencies
run: |
cd apps/${{ inputs.app }}
pnpm install --frozen-lockfile
- name: Lint & Type Check
run: |
cd apps/${{ inputs.app }}
pnpm lint
pnpm typecheck
- name: Run tests
run: |
cd apps/${{ inputs.app }}
pnpm test:unit --coverage
- name: Check coverage
run: |
cd apps/${{ inputs.app }}
pnpm test:coverage-check
build:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Install dependencies
run: |
cd apps/${{ inputs.app }}
pnpm install --frozen-lockfile
- name: Build
run: |
cd apps/${{ inputs.app }}
pnpm build:versioned
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.app }}-dist
path: apps/${{ inputs.app }}/dist/
retention-days: 7
deploy-production:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ inputs.app }}-dist
path: dist/
- name: Deploy to CDN
run: |
VERSION=$(node -p "require('./package.json').version")
aws s3 sync dist/ s3://micro-frontend-production/apps/${{ inputs.app }}/ --delete
aws cloudfront create-invalidation \
--distribution-id $CLOUDFRONT_ID \
--paths "/apps/${{ inputs.app }}/*"
- name: Start Gray Release
if: inputs.strategy == 'gray'
run: |
curl -X POST https://portal-api.company.com/api/gray/start \
-H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"appName": "${{ inputs.app }}",
"strategy": "percentage",
"percentage": 10
}'
- name: Immediate Release
if: inputs.strategy == 'immediate'
run: |
curl -X POST https://portal-api.company.com/api/gray/finish \
-H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"appName": "${{ inputs.app }}"}'
- name: Health Check
run: |
sleep 30
curl -f https://portal.company.com/api/health/${{ inputs.app }} || exit 1
notify:
needs: deploy-production
runs-on: ubuntu-latest
steps:
- name: Send deployment notification
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "部署完成: ${{ inputs.app }} → production (策略: ${{ inputs.strategy }})",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🚀 *${{ inputs.app }}* 已部署到 *production*\\n策略: `${{ inputs.strategy }}`\\n触发者: ${{ github.actor }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
通过本文,你已经掌握了微前端架构下的完整 CI/CD 与灰度发布体系:
- 子应用独立构建流水线
- 多环境部署策略(dev/staging/preprod/production)
- 版本管理与依赖兼容性校验
- 灰度发布方案(白名单/流量比例/用户属性)
- 动态注册中心配置
- 发布监控与自动回滚
- 审批流程与一键部署