Skip to content

前端自动化流程的实践

CI/CD 从开发到部署的实践。核心流程:带着问题去找答案,通过答案解决具体问题。

一、问题思考:要解决哪些问题?

1.1 核心问题识别

开发效率问题:

  • 手动构建部署耗时耗力
  • 环境不一致导致的"在我机器上能跑"
  • 多人协作时的代码冲突和集成困难

代码质量问题:

  • 代码规范难以统一执行
  • 潜在 bug 进入生产环境
  • 依赖更新带来的兼容性问题

部署运维问题:

  • 部署过程复杂易出错
  • 回滚困难,风险高
  • 监控缺失,问题发现滞后

1.2 自动化目标

目标领域具体目标衡量指标
代码质量自动化测试覆盖 > 80%测试通过率、代码覆盖率
构建效率构建时间 < 5 分钟构建时长、构建成功率
部署安全零手动干预部署部署成功率、回滚时间
监控能力5 分钟内发现问题MTTR、错误率
团队协作PR 合并到部署 < 30 分钟部署频率、交付周期

二、实现思路:怎么才能实现?

问题梳理

  1. 代码质量:怎么保证代码质量?
  2. 工具:用哪些工具?
  3. 配置:怎么配置?
  4. 集成:怎么集成?
  5. 部署:怎么部署?
  6. 监控:怎么监控?
  7. 回滚:怎么回滚?
  8. 其他:还有什么?

2.1 工具链选型

核心工具栈

yaml
# 完整工具链配置示例(注意:配置示例仅供参考,具体配置需根据项目需求进行调整)
toolchain:
  version_control:
    - git
    - git-flow (分支模型)

  package_management:
    - npm/yarn/pnpm
    - nvm (Node版本管理)

  code_quality:
    - ESLint (代码规范)
    - Prettier (代码格式化)
    - Stylelint (CSS规范)
    - Commitlint (提交规范)

  testing:
    - Jest (单元测试)
    - React Testing Library (组件测试)
    - Cypress (E2E测试)
    - Playwright (跨浏览器测试)

  building:
    - Vite/Webpack (构建工具)
    - Babel/TypeScript (代码编译)
    - esbuild/SWC (构建加速)

  quality_gate:
    - SonarQube (代码分析)
    - Lighthouse (性能检测)

  ci_cd:
    - GitHub Actions (CI/CD平台)
    - Jenkins (企业级可选)
    - ArgoCD (GitOps)

  containerization:
    - Docker (容器化)
    - Docker Compose (本地开发)

  orchestration:
    - Kubernetes (容器编排)
    - Helm (K8s包管理)

  monitoring:
    - Sentry (错误监控)
    - Prometheus (指标监控)
    - Grafana (数据可视化)
    - ELK Stack (日志分析)

  notification:
    - Slack/DingTalk (通知)
    - Email (报告)

工具选型决策矩阵

mermaid
graph TD
    A[项目需求分析] --> B{项目规模}
    B -->|小型项目| C[GitHub Actions + Vercel]
    B -->|中型项目| D[GitLab CI + Kubernetes]
    B -->|大型企业级| E[Jenkins + ArgoCD + 私有云]

    C --> F[快速部署<br/>低成本]
    D --> G[灵活可控<br/>中等成本]
    E --> H[高可用性<br/>高成本]

2.2 配置实践

2.2.1 Git 工作流配置

bash
# .gitlab-ci.yml 或 GitHub Actions 工作流示例
# Git Hook配置
#!/bin/bash
# pre-commit hook
npm run lint-staged
npm run type-check

# commit-msg hook
npx commitlint --edit "$1"

2.2.2 代码质量配置

javascript
// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:prettier/recommended'
  ],
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    'prettier/prettier': ['error', {
      singleQuote: true,
      trailingComma: 'es5',
      printWidth: 100
    }]
  }
};

// .huskyrc 或 package.json 中
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss,less}": ["stylelint --fix", "prettier --write"],
    "*.{json,md}": ["prettier --write"]
  }
}

2.2.3 测试配置

javascript
// jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
  collectCoverageFrom: [
    "src/**/*.{js,jsx,ts,tsx}",
    "!src/**/*.d.ts",
    "!src/index.tsx",
    "!src/reportWebVitals.ts",
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

// cypress.config.js
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
    supportFile: "cypress/support/e2e.ts",
    setupNodeEvents(on, config) {
      // 实现节点事件监听器
    },
  },
  component: {
    devServer: {
      framework: "react",
      bundler: "vite",
    },
  },
});

2.3 CI/CD 流水线设计

2.3.1 完整流水线示例

yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  release:
    types: [published]

env:
  NODE_VERSION: "18"
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 代码质量检查
  code-quality:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Unit tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3

      - name: SonarCloud Scan
        uses: SonarSource/sonarcloud-github-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

  # 构建和测试
  build-and-test:
    needs: code-quality
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NODE_ENV: production

      - name: Archive build
        uses: actions/upload-artifact@v3
        with:
          name: build-${{ matrix.node-version }}
          path: dist

      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9
        with:
          configPath: "./lighthouserc.json"
          uploadArtifacts: true
          temporaryPublicStorage: true

  # 容器化构建
  docker-build:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=sha,prefix={{branch}}-
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 部署到测试环境
  deploy-staging:
    needs: docker-build
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to Staging
        run: |
          echo "Deploying to staging environment"
          # 这里可以是 kubectl, helm, terraform 等命令
          kubectl set image deployment/myapp \
            myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      - name: Run E2E tests
        run: |
          npm run test:e2e

      - name: Health check
        run: |
          ./scripts/health-check.sh https://staging.example.com

  # 部署到生产环境
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    if: github.event_name == 'release'

    steps:
      - name: Approval
        uses: trstringer/manual-approval@v1
        with:
          secret: ${{ secrets.APPROVAL_SECRET }}
          approvers: "team-lead,product-owner"

      - name: Deploy to Production
        run: |
          echo "Deploying to production"
          kubectl apply -f k8s/production/

      - name: Run smoke tests
        run: |
          npm run test:smoke

      - name: Notify team
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          author_name: Deployment Bot

2.3.2 多环境配置管理

typescript
// config/environments.ts
export interface EnvironmentConfig {
  apiUrl: string;
  cdnUrl: string;
  sentryDsn: string;
  analyticsId: string;
  featureFlags: Record<string, boolean>;
}

const environments: Record<string, EnvironmentConfig> = {
  development: {
    apiUrl: "http://localhost:3000/api",
    cdnUrl: "http://localhost:3000",
    sentryDsn: "",
    analyticsId: "UA-DEV-123",
    featureFlags: {
      enableBeta: true,
      enableAnalytics: false,
    },
  },
  staging: {
    apiUrl: "https://api.staging.example.com",
    cdnUrl: "https://cdn.staging.example.com",
    sentryDsn: process.env.SENTRY_DSN_STAGING,
    analyticsId: "UA-STAGING-456",
    featureFlags: {
      enableBeta: true,
      enableAnalytics: true,
    },
  },
  production: {
    apiUrl: "https://api.example.com",
    cdnUrl: "https://cdn.example.com",
    sentryDsn: process.env.SENTRY_DSN_PRODUCTION,
    analyticsId: "UA-PRODUCTION-789",
    featureFlags: {
      enableBeta: false,
      enableAnalytics: true,
    },
  },
};

export const getConfig = (): EnvironmentConfig => {
  const env = process.env.NODE_ENV || "development";
  return environments[env];
};

2.4 部署策略

2.4.1 部署模式对比

yaml
deployment_strategies:
  rolling_update:
    description: "零停机更新,逐步替换实例"
    pros:
      - 无停机时间
      - 版本共存,便于回滚
    cons:
      - 需要额外资源
      - 可能同时存在多版本

  blue_green:
    description: "准备完整新环境,一次性切换流量"
    pros:
      - 快速回滚
      - 彻底的环境隔离
    cons:
      - 需要双倍资源
      - 数据库迁移复杂

  canary:
    description: "逐步向小部分用户发布新版本"
    pros:
      - 风险控制好
      - 实时监控效果
    cons:
      - 需要智能路由
      - 配置复杂

  feature_flags:
    description: "代码部署但功能通过开关控制"
    pros:
      - 灵活的功能发布
      - 无需代码回滚
    cons:
      - 代码复杂度增加
      - 需要管理开关

2.4.2 Kubernetes 部署配置

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-app
  namespace: production
spec:
  replicas: 3
  revisionHistoryLimit: 5 # 保留5个历史版本用于回滚
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
        version: v1.0.0
    spec:
      containers:
        - name: frontend
          image: ghcr.io/org/app:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
          envFrom:
            - configMapRef:
                name: frontend-config
            - secretRef:
                name: frontend-secrets
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 5

2.5 监控体系

2.5.1 前端监控配置

javascript
// src/monitoring/index.ts
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import { Metrics } from './metrics';
import { PerformanceMonitor } from './performance';
import { ErrorTracker } from './errors';

export class MonitoringSystem {
  private static instance: MonitoringSystem;

  static init() {
    if (process.env.NODE_ENV === 'production') {
      Sentry.init({
        dsn: process.env.REACT_APP_SENTRY_DSN,
        integrations: [new BrowserTracing()],
        tracesSampleRate: 0.1,
        environment: process.env.NODE_ENV,
        release: process.env.REACT_APP_VERSION,
      });

      this.instance = new MonitoringSystem();
      this.instance.startPerformanceMonitoring();
    }
  }

  private startPerformanceMonitoring() {
    // 监控核心Web指标
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'largest-contentful-paint') {
          Metrics.trackLCP(entry.startTime);
        } else if (entry.entryType === 'first-input') {
          Metrics.trackFID(entry.startTime);
        }
      }
    });

    observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });
  }

  static trackError(error: Error, context?: any) {
    if (this.instance) {
      ErrorTracker.track(error, context);
    }
  }

  static trackMetric(name: string, value: number, tags?: Record<string, string>) {
    if (this.instance) {
      Metrics.track(name, value, tags);
    }
  }
}

2.5.2 告警规则示例

yaml
# alert-rules.yaml
groups:
  - name: frontend_alerts
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "高错误率检测"
          description: "错误率超过10%,当前值 {{ $value }}"

      - alert: SlowPageLoad
        expr: histogram_quantile(0.95, rate(page_load_duration_bucket[5m])) > 3000
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "页面加载缓慢"
          description: "95分位页面加载时间超过3秒"

      - alert: HighJSBundleSize
        expr: frontend_bundle_size_bytes > 5e6
        labels:
          severity: warning
        annotations:
          summary: "JS包体积过大"
          description: "主包体积超过5MB"

2.6 回滚机制

2.6.1 自动回滚策略

yaml
# 回滚策略配置
rollback_strategy:
  automated_rollback:
    triggers:
      - error_rate_increase: "5分钟内错误率增加超过50%"
      - latency_increase: "平均响应时间增加超过100%"
      - health_check_failure: "连续3次健康检查失败"

  manual_rollback:
    triggers:
      - user_feedback: "大量用户报告问题"
      - business_metrics: "关键业务指标下降"
      - security_issues: "发现安全漏洞"

  rollback_steps:
    - step1: "暂停新流量"
    - step2: "检查当前状态"
    - step3: "执行回滚命令"
    - step4: "验证回滚结果"
    - step5: "通知相关人员"

2.6.2 Kubernetes 自动回滚

bash
#!/bin/bash
# auto-rollback.sh

# 检查应用状态
check_application_health() {
  local namespace=$1
  local deployment=$2

  # 检查Pod状态
  local ready_pods=$(kubectl get deployment -n $namespace $deployment \
    -o jsonpath='{.status.readyReplicas}')
  local total_pods=$(kubectl get deployment -n $namespace $deployment \
    -o jsonpath='{.status.replicas}')

  if [ "$ready_pods" -lt "$total_pods" ]; then
    echo "Not all pods are ready"
    return 1
  fi

  # 检查错误率
  local error_rate=$(curl -s http://prometheus:9090/api/v1/query \
    --data-urlencode 'query=rate(http_requests_total{status=~"5.."}[5m])' \
    | jq -r '.data.result[0].value[1]')

  if (( $(echo "$error_rate > 0.1" | bc -l) )); then
    echo "Error rate too high: $error_rate"
    return 1
  fi

  return 0
}

# 执行回滚
perform_rollback() {
  local namespace=$1
  local deployment=$2

  echo "Initiating rollback for $deployment in namespace $namespace"

  # 回滚到上一个版本
  kubectl rollout undo deployment/$deployment -n $namespace

  # 等待回滚完成
  kubectl rollout status deployment/$deployment -n $namespace --timeout=300s

  if [ $? -eq 0 ]; then
    echo "Rollback completed successfully"

    # 发送通知
    send_notification "Rollback Successful" \
      "Deployment $deployment rolled back to previous version"
  else
    echo "Rollback failed"
    send_notification "Rollback Failed" \
      "Manual intervention required for $deployment"
    exit 1
  fi
}

# 主逻辑
main() {
  local namespace=${NAMESPACE:-default}
  local deployment=${DEPLOYMENT:-frontend}
  local check_interval=${CHECK_INTERVAL:-60}

  while true; do
    if ! check_application_health $namespace $deployment; then
      perform_rollback $namespace $deployment
      break
    fi

    sleep $check_interval
  done
}

2.7 其他重要实践

2.7.1 文档即代码

markdown
# 自动化文档

项目中的所有配置和流程都应文档化:

## 开发环境设置

```bash
# 一键环境设置
make setup
```

部署流程

  • 测试环境:自动触发于 develop 分支
  • 生产环境:需要人工审批

监控看板

  • Grafana: https://grafana.example.com
  • Sentry: https://sentry.example.com

#### 2.7.2 安全扫描集成
```yaml
# security-scan.yml
name: Security Scan

on: [push, pull_request]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Run Snyk to check for vulnerabilities
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

      - name: Run OWASP Dependency Check
        uses: dependency-check/Dependency-Check_Action@main
        with:
          project: 'frontend-app'
          path: '.'
          format: 'HTML'

      - name: Run npm audit
        run: npm audit --audit-level=high

      - name: Check for secrets in code
        uses: gitleaks/gitleaks-action@v2

2.7.3 性能预算

javascript
// .performance-budget.json
{
  "budgets": [
    {
      "resourceType": "document",
      "budget": 50 // 最大50KB
    },
    {
      "resourceType": "script",
      "budget": 200 // 最大200KB
    },
    {
      "resourceType": "stylesheet",
      "budget": 50 // 最大50KB
    },
    {
      "resourceType": "image",
      "budget": 500 // 最大500KB
    },
    {
      "resourceType": "font",
      "budget": 100 // 最大100KB
    },
    {
      "resourceType": "media",
      "budget": 1000 // 最大1MB
    },
    {
      "resourceType": "third-party",
      "budget": 250 // 最大250KB
    }
  ],
  "timings": {
    "firstContentfulPaint": 1800, // 1.8秒
    "largestContentfulPaint": 2500, // 2.5秒
    "cumulativeLayoutShift": 0.1, // CLS
    "interactive": 3800 // 3.8秒
  }
}

三、真实案例:基于 GitLab CI/CD 的前端自动化部署

使用 docker + gitlab CI/CD + nginx + docker-compose(镜像包)

切记:不要盲目追随某种流程,而是根据团队和项目的实际需求选择或定制合适的流程

团队开发背景:人员不多,项目迭代频繁,需要快速响应需求变更。

yaml
# 真正的简洁方案
toolchain:
  version_control: git # 只需要git,现代GitHub/GitLab Flow足够

  building:
    - Vite # 现代化构建,开箱即用
    - TypeScript # 类型检查

  ci_cd:
    - GitHub Actions # 免费,配置简单
    # 或 GitLab CI

  deployment:
    - Vercel # 一键部署
    - Docker # 简单容器化(可选)
    # 或 Netlify

  monitoring:
    - Sentry (免费版) # 错误监控

  communication:
    - GitHub Issues # 内置项目管理