微前端进阶(四)

微前端进阶(四)

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


目录

  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)
  • 版本管理与依赖兼容性校验
  • 灰度发布方案(白名单/流量比例/用户属性)
  • 动态注册中心配置
  • 发布监控与自动回滚
  • 审批流程与一键部署
相关推荐
geovindu13 小时前
css: Timeline scribble
前端·css·iphone
沙漠之主21 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty21 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js
Csvn1 天前
diff 算法(虚拟 DOM Reconciliation)
前端
前端snow1 天前
ai agent --- 文件存储
前端
Frag0ut1 天前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
IT_陈寒1 天前
SpringBoot自动配置坑了我一把,原来是这样绕过去的
前端·人工智能·后端
广州华水科技1 天前
单北斗GNSS变形监测在大坝安全监测中的应用与优势
前端
zeng不错1 天前
B站 173 个投稿活动看到眼瞎?我写了个扩展,主打一个精准打击
前端
Latchh1 天前
前端导出的JPEG红字发糊,质量要拉到100才清楚
前端·图像处理·人工智能·计算机视觉