CI/CD与部署

What — 是什么

CI/CD 是持续集成和持续交付/部署的实践,通过自动化流水线完成代码检查、测试、构建和部署,确保每次变更安全可靠地到达用户。

核心概念:

  • CI(持续集成):代码提交后自动运行 lint、测试、构建
  • CD(持续交付):自动化到预发布环境,手动批准后发布
  • CD(持续部署):全自动化,代码合并后直接发布到生产
  • 流水线(Pipeline):由多个 Stage 组成的自动化流程
  • 环境:development → staging → production

关键特性:

  • CI/CD 是工程化的最后一公里
  • 前端部署的核心:构建产物 → CDN/静态服务器
  • 现代部署方式:容器化、Serverless、Edge Functions

Why — 为什么

适用场景:

  • 所有团队协作的项目
  • 需要频繁发布的项目
  • 需要质量门禁的项目

对比方案:

维度GitHub ActionsGitLab CIJenkinsVercel
配置方式YAMLYAMLGroovy/Jenkinsfile零配置
托管GitHub 托管GitLab 托管自建Vercel 托管
生态GitHub 集成GitLab 集成插件丰富Next.js 最优
学习成本极低
灵活性极高
免费额度2000 min/月400 min/月无限(自建)免费额度

优缺点:

  • ✅ 优点:
    • 自动化减少人为错误
    • 质量门禁保证代码质量
    • 快速回滚能力
  • ❌ 缺点:
    • 流水线配置有学习成本
    • 自建 CI 有运维负担
    • 调试流水线问题不如本地直观

How — 怎么用

GitHub Actions

基础 CI 流水线:

# .github/workflows/ci.yml
name: CI

on:
    push:
        branches: [main, develop]
    pull_request:
        branches: [main]

jobs:
    check:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4

            - uses: actions/setup-node@v4
              with:
                  node-version: 20
                  cache: 'pnpm'

            - run: pnpm install --frozen-lockfile

            - name: Lint
              run: pnpm lint

            - name: Type Check
              run: pnpm type-check

            - name: Unit Tests
              run: pnpm test:ci

            - name: Build
              run: pnpm build

            - name: Upload Coverage
              uses: codecov/codecov-action@v4
              with:
                  token: ${{ secrets.CODECOV_TOKEN }}

部署到生产:

# .github/workflows/deploy.yml
name: Deploy

on:
    push:
        branches: [main]

jobs:
    deploy:
        runs-on: ubuntu-latest
        needs: [check] # 依赖 CI 通过
        environment: production
        steps:
            - uses: actions/checkout@v4

            - uses: actions/setup-node@v4
              with:
                  node-version: 20
                  cache: 'pnpm'

            - run: pnpm install --frozen-lockfile
            - run: pnpm build

            - name: Deploy to CDN
              run: |
                  aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }} \
                    --delete \
                    --cache-control "public, max-age=31536000, immutable" \
                    --exclude "index.html"
                  aws s3 cp ./dist/index.html s3://${{ secrets.S3_BUCKET }}/index.html \
                    --cache-control "public, max-age=0, s-maxage=60"

            - name: Invalidate CDN Cache
              run: |
                  aws cloudfront create-invalidation \
                    --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
                    --paths "/index.html"

Preview 部署(PR 预览):

# .github/workflows/preview.yml
name: Preview

on:
    pull_request:

jobs:
    preview:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                  node-version: 20
                  cache: 'pnpm'

            - run: pnpm install --frozen-lockfile
            - run: pnpm build

            - name: Deploy Preview
              id: deploy
              uses: amondnet/vercel-action@v25
              with:
                  vercel-token: ${{ secrets.VERCEL_TOKEN }}
                  vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
                  vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}

            - name: Comment PR
              uses: actions/github-script@v7
              with:
                  script: |
                      github.rest.issues.createComment({
                          issue_number: context.issue.number,
                          owner: context.repo.owner,
                          repo: context.repo.repo,
                          body: `Preview: ${{ steps.deploy.outputs.preview-url }}`
                      });

Docker 容器化部署

# Dockerfile — 多阶段构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# 生产镜像:仅 Nginx + 构建产物
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # SPA 路由:所有路径回退到 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源长缓存(文件名含 hash)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # index.html 不缓存
    location = /index.html {
        add_header Cache-Control "no-cache";
    }

    # gzip
    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
}

Vercel 零配置部署

# 安装 CLI
npm i -g vercel

# 部署
vercel          # 预览部署
vercel --prod   # 生产部署
// vercel.json — 自定义配置
{
    "framework": "vite",
    "buildCommand": "pnpm build",
    "outputDirectory": "dist",
    "rewrites": [
        { "source": "/(.*)", "destination": "/index.html" }
    ],
    "headers": [
        {
            "source": "/assets/(.*)",
            "headers": [
                { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
            ]
        }
    ]
}

版本管理与回滚

# 语义化版本
npm version patch  # 1.0.0 → 1.0.1(Bug 修复)
npm version minor  # 1.0.0 → 1.1.0(新功能)
npm version major  # 1.0.0 → 2.0.0(破坏性变更)

# 回滚到上一版本(Vercel)
vercel rollback

# 回滚到上一版本(Docker + K8s)
kubectl rollout undo deployment/my-app

# 回滚到上一版本(Git)
git revert HEAD
git push origin main

环境变量管理

# GitHub Actions: 环境变量 + Secrets
jobs:
    deploy:
        environment: production
        steps:
            - name: Build with env
              run: pnpm build
              env:
                  VITE_API_URL: ${{ secrets.API_URL }}
                  VITE_COMMIT_SHA: ${{ github.sha }}

            - name: Sentry Upload
              run: pnpm sentry-upload
              env:
                  SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
// vite.config.ts — 构建时注入
export default defineConfig({
    define: {
        __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
        __COMMIT_SHA__: JSON.stringify(process.env.VITE_COMMIT_SHA),
    },
});

常见问题与踩坑

问题原因解决方案
构建缓存失效每次全新安装依赖利用 CI 缓存(actions/setup-node 的 cache)
部署后白屏路由未配置回退Nginx try_files / Vercel rewrites
环境变量泄露构建产物中包含敏感信息只用 VITE_ 前缀的变量,密钥走后端
并行部署冲突多人同时合并到 main锁定部署环境,串行执行
CDN 缓存不更新index.html 被缓存index.html 设 no-cache,JS/CSS 文件名含 hash

最佳实践

  • CI 必须包含 lint + type-check + test + build
  • PR 触发 Preview 部署,main 触发生产部署
  • 静态资源文件名含 hash + 长缓存,index.html 不缓存
  • 环境变量分环境管理,敏感信息用 Secrets
  • 容器化部署用多阶段构建,生产镜像只含 Nginx + 产物

面试题

Q1: CI 和 CD 分别是什么?持续交付和持续部署有什么区别?

CI(持续集成)是代码提交后自动运行 lint、测试、构建,确保变更不破坏主分支;CD 包含持续交付(Continuous Delivery,自动化到预发布环境,手动批准后发布)和持续部署(Continuous Deployment,全自动化直达生产)。区别在于最后一步是否需要人工审批。

Q2: 蓝绿部署和滚动部署的区别是什么?

蓝绿部署维护两套完整环境(蓝/绿),切换流量实现零停机发布,回滚只需切回旧环境,但需要双倍资源;滚动部署逐步替换旧版本实例,资源占用少,但新旧版本会短暂共存,需注意接口兼容性,回滚较慢需逐个替换回去。

Q3: Docker 多阶段构建的作用是什么?

多阶段构建用多个 FROM 指令将构建环境和运行环境分离:第一阶段用完整 Node 镜像安装依赖和构建,第二阶段仅用轻量 Nginx/Alpine 镜像 + 构建产物。最终镜像不含源码和 node_modules,体积从 GB 级降到 MB 级,减少攻击面,部署更快。

Q4: 前端项目的环境变量如何安全管理?

构建时注入(Vite 的 VITE_ 前缀变量、Webpack 的 DefinePlugin),非 VITE_ 前缀的变量不会暴露到前端;敏感信息(API 密钥、Token)存放在 CI 的 Secrets 中,仅构建时注入,不写入代码仓库;index.html 不缓存 + 静态资源含 hash 确保 CDN 更新。


相关链接: