微前端进阶(四)

微前端进阶(四)

子应用独立构建、多环境部署、灰度发布与版本管理全攻略


目录

  1. [微前端 CI/CD 设计原则](#微前端 CI/CD 设计原则)
  2. 子应用独立构建流水线
  3. 多环境部署策略
  4. 版本管理与依赖兼容性
  5. 灰度发布方案
  6. 子应用注册中心动态配置
  7. 监控与回滚机制
  8. 完整流水线实战

一、微前端 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)
  • 版本管理与依赖兼容性校验
  • 灰度发布方案(白名单/流量比例/用户属性)
  • 动态注册中心配置
  • 发布监控与自动回滚
  • 审批流程与一键部署
相关推荐
罗超驿42 分钟前
18.Web API 实战:元素与表单属性的获取和修改
开发语言·前端·javascript
无风听海1 小时前
JSON Web Token(JWT)完全指南
java·前端·json
IT_陈寒1 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端
IT_陈寒1 小时前
Java注解空指针?这个坑我踩得莫名其妙
前端·人工智能·后端
H0r1zon.2 小时前
PinCopy:双击 Ctrl,把剪贴板「钉」在屏幕上
前端
kyriewen2 小时前
大厂面试新规:不会用AI编程,直接挂
前端·面试·ai编程
努力找实习的前端小白2 小时前
useImperativeHandle,useRef,forwardRef的协作关系
前端·面试
ZengLiangYi2 小时前
Fastify 加 Electron:把 Web 服务嵌进桌面应用
前端·javascript·后端
qq_2518364573 小时前
基于nodejs express +vue 天天商城系统设计与实现 (源码 文档)
前端·vue.js·express