React 应用的持续集成(CI/CD):利用 GitHub Actions 实现从全栈代码提交到自动化部署的闭环

好,欢迎来到今天的“全栈代码生死时速”研讨会。我是你们的讲师,一个曾经把代码部署到一半咖啡洒在键盘上,导致整个生产环境蓝屏,最后不得不半夜爬起来修 Bug 的资深“老司机”。

今天我们不聊花里胡哨的框架新特性,我们来聊聊那个让无数人从“晨间咖啡”变成“深夜泡面”的终极话题:持续集成(CI)与持续部署(CD)。特别是当你手里有一个 React 前端,加上一个 Node/Python/Go 后端,还有一个甚至还没配好的数据库时,如何利用 GitHub Actions 把这一坨乱麻变成一个自动化的瑞士军刀?

咱们直接切入正题。

第 1 节:仓库布局——别让你的代码像垃圾堆

在写任何一行 CI/CD 的 YAML 代码之前,你的代码仓库得像个正经的仓库。如果你的项目结构是这样的:

project/
├── index.html
├── package.json
├── server.js
├── api.py
└── .env

那我劝你先别往下看,先去把你的项目重构一下。在全栈世界里,前端和后端是两个需要被尊重的物种,它们不应该在同一个文件里互相问候。

我们要搞一个 Monorepo(单体仓库) 的结构。这意味着所有的代码都在一个仓库里,但通过文件夹来区分。这是目前 GitHub Actions 处理全栈项目最优雅的方式。想象一下,你把前端和后端关在同一个笼子里,但中间隔了一道铁丝网,谁也别想越狱。

标准的“专家级”目录长这样:

my-awesome-app/
├── frontend/              # 前端 React 代码
│   ├── package.json
│   ├── src/
│   ├── public/
│   └── Dockerfile         # 前端也要 Docker 化,别跟我抬杠
├── backend/               # 后端 API 代码
│   ├── package.json
│   ├── src/
│   ├── Dockerfile         # 后端也要 Docker 化
│   └── .env.example
├── shared/                # 共享的 UI 组件库或工具类
│   └── package.json
├── docker-compose.yml     # 本地开发的神器
└── .github/
    └── workflows/
        └── ci-cd.yml      # 就是我们今天的主角

为什么要这样?因为当你提交代码时,GitHub Actions 不仅仅是跑一个 npm run build。它需要知道:嘿,这代码是前端?还是后端?或者是我们新加的共享组件?如果是共享组件,是不是前后端都要用?如果是,那就别傻乎乎地分别构建,浪费服务器资源。

第 2 节:前端 CI——把前端变成一个听话的乖宝宝

前端这东西,通常比较自由,想怎么写怎么写。但在 CI 里,它必须像个经过严格军训的士兵。我们要做的就是给它立规矩。

2.1 环境设置与缓存

每次部署,CI 机器都是全新的。这意味着它没有 node_modules,也没有缓存。每次运行 npm install 都像是在用一台老旧的拨号上网电脑下载整个互联网。

为了防止 GitHub Actions 服务器吐血,我们必须开启缓存。

- name: Setup Node
  uses: actions/setup-node@v4
  with:
    node-version: '20' # 永远不要在 CI 里用最新版,用 LTS 或者你项目里指定的版本
    cache: 'npm'       # 这一行魔法,能加速 90% 的安装时间

- name: Install Dependencies
  run: npm ci
  working-directory: ./frontend

npm ci 是比 npm install 更适合 CI 的命令,因为它会严格按照 package-lock.json 来安装,确保每次安装结果都一样。

2.2 Linting——代码风格检查

这不仅仅是强迫症,这是为了团队和谐。如果一个人写了 if (error) {,另一个人写了 if (error){,或者缩进忽左忽右,这会让代码审查变得极其痛苦。

我们用 ESLint 和 Prettier。

- name: Lint Code
  run: npm run lint
  working-directory: ./frontend

如果你的 ESLint 配置得当,这一步会在你试图提交带 Bug 代码时直接阻断流程。这就像是你家大门的锁,没有钥匙谁也别想进。

2.3 Testing——测试是你的保镖

这是最重要的一步。你写的 React 组件,是不是只在你那台屏幕显示效果最好的 MacBook 上能跑?别逗了。

CI 环境通常是 Linux 服务器。如果你的代码用了特定的 CSS 特性,或者浏览器兼容性问题,这里会立刻暴露出来。

- name: Run Tests
  run: npm test -- --watchAll=false
  working-directory: ./frontend

注意 --watchAll=false。这个参数是关键。默认的 Jest 会进入监听模式,当你改一行代码,它就跑一遍。但在 CI 里,我们不想让它监听,我们只想让它跑完就死,立刻,马上,告诉我是 Pass 还是 Fail。

第 3 节:后端 CI——别烧了你的数据库

如果说前端是花瓶,那后端就是承重墙。后端 CI 的核心在于:隔离

后端通常依赖数据库。在 CI 环境里,你不能真的去连生产环境的数据库,那会炸掉。你需要一个“虚拟的”数据库,或者一个“临时的”数据库。

3.1 Docker Compose 的魔力

大多数全栈项目(MERN/MEAN/PERN 等)都支持 Docker Compose。我们可以利用 GitHub Actions 的 services 功能来启动临时的数据库服务。

比如,你的后端连接的是 MongoDB。在你的 GitHub Action 里,你可以启动一个临时的 MongoDB 实例:

services:
  mongodb:
    image: mongo:6.0
    ports:
      - 27017:27017
    env:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example

  # 你的后端测试任务
  backend-test:
    runs-on: ubuntu-latest
    services:
      # ... 同上,复用 MongoDB 配置
    steps:
      - name: Run Backend Tests
        run: npm test
        working-directory: ./backend

这样做的好处是,你的测试代码不需要大改。你只需要确保你的 docker-compose.yml 在本地能跑,通常在 CI 里也能跑(前提是你配置正确)。

3.2 类型检查

如果你用 TypeScript,这一步是强制性的。TypeScript 的类型检查就是代码的免疫系统。如果你强行绕过它,等运行到生产环境,某个后端参数传错,整个 API 就会报错。CI 就是你最后的防线。

- name: Type Check
  run: npm run type-check
  working-directory: ./backend

第 4 节:构建与容器化——Docker,万物之基

现在,你的前端通过了 lint,后端通过了测试。接下来是那个让人既爱又恨的环节:构建。

在 Web 开发中,构建通常意味着把漂亮的源代码转换成一堆冷冰冰的、压缩过的、几乎无法阅读的 JS 文件。Webpack、Vite、Rollup 都在干这事。

4.1 前端构建

React 项目通常有一个 build 脚本。我们把它搬到 CI 里执行。

- name: Build Frontend
  run: npm run build
  working-directory: ./frontend

构建完成后,frontend/build 目录里会有静态文件。这些文件就是最终部署给用户看的。

4.2 Docker 化(进阶)

我知道,新手会问:“为什么要 Docker?我直接把 build 文件扔到 Nginx 服务器上不行吗?”

行,你是对的,对于简单的个人项目,行。但如果你想证明自己是“资深专家”,你就必须懂 Docker。

前端 Dockerfile 通常非常简单,因为 Node.js 体积很大,而我们只需要浏览器能跑的 JS 文件。我们会用多阶段构建来瘦身:

# 第一阶段:构建
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 第二阶段:生产环境
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

后端 Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]

为什么要 Docker?因为这样你就不用管生产服务器上装没装 Node.js,装的是 14 还是 18,甚至是不装。Docker 保证环境的一致性。这就是所谓的“在我的机器上能跑”的终结者。

第 5 节:CD——自动化部署的“核按钮”

好了,现在我们有了两个 Docker 镜像:前端镜像和后端镜像。接下来,我们要把它们推到仓库里,然后推到云服务器上去。

5.1 推送镜像到 GitHub Container Registry (GHCR)

首先,我们需要给镜像打个标签,然后推送到 GHCR。

- name: Login to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and Push Frontend
  run: |
    docker build -t ghcr.io/${{ github.repository }}/frontend:latest ./frontend
    docker push ghcr.io/${{ github.repository }}/frontend:latest

- name: Build and Push Backend
  run: |
    docker build -t ghcr.io/${{ github.repository }}/backend:latest ./backend
    docker push ghcr.io/${{ github.repository }}/backend:latest

注意那个 secrets.GITHUB_TOKEN。这是 GitHub 给你的免密通行证。你不需要自己写密码。

5.2 远程部署

有了镜像,我们怎么告诉服务器去拉取并重启?这时候,我们需要一个 SSH 连接。

但是,别把 SSH 私钥直接写在 YAML 文件里!那是自杀行为。你必须在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 里,添加一个叫 SSH_PRIVATE_KEY 的密钥。

然后,利用一个叫 appleboy/ssh-action 的工具,在远程服务器上执行命令。

- name: Deploy to Server
  uses: appleboy/[email protected]
  with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USER }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    script: |
      cd /var/www/my-app
      docker-compose pull
      docker-compose up -d --remove-orphans

这段脚本的意思是:SSH 登录到你的服务器,进入项目目录,拉取最新的镜像,然后重启容器。搞定。整个过程不需要你动一下鼠标,甚至不需要你喝完那杯咖啡。

第 6 节:实战演练——终极 GitHub Actions Workflow

好了,理论说多了大家也困。现在我们把所有东西串起来,写一个完整的、包含前端、后端、Docker、部署的“核武器”级别的 YAML 文件。

注意: 这里的逻辑是:如果前端测试没过,或者后端测试没过,整个流程就 Stop。只有两边都健康了,才进入构建和部署阶段。

name: Full Stack CI/CD Pipeline

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  # ==========================================
  # Job 1: 前端质量检测
  # ==========================================
  frontend-lint-test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Lint Code
        run: npm run lint

      - name: Run Tests
        run: npm test -- --watchAll=false --coverage

  # ==========================================
  # Job 2: 后端质量检测与构建
  # ==========================================
  backend-lint-test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend

    services:
      # 启动一个临时的 MongoDB
      mongodb:
        image: mongo:6.0
        ports:
          - 27017:27017
        env:
          MONGO_INITDB_ROOT_USERNAME: root
          MONGO_INITDB_ROOT_PASSWORD: example

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

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

      - name: Run Tests
        run: npm test

      - name: Build Backend
        run: npm run build

  # ==========================================
  # Job 3: 镜像构建与部署
  # ==========================================
  docker-build-and-deploy:
    needs: [frontend-lint-test, backend-lint-test] # 依赖前两个 Job 通过
    if: github.ref == 'refs/heads/main'            # 只有在 main 分支才部署
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Frontend
        run: |
          docker build -t ghcr.io/${{ github.repository }}/frontend:latest ./frontend
          docker push ghcr.io/${{ github.repository }}/frontend:latest

      - name: Build and Push Backend
        run: |
          docker build -t ghcr.io/${{ github.repository }}/backend:latest ./backend
          docker push ghcr.io/${{ github.repository }}/backend:latest

      - name: Deploy to Server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/my-awesome-app
            docker-compose pull
            docker-compose up -d --remove-orphans
            echo "Deployment Successful!"

第 7 节:安全与最佳实践——别让你的服务器被黑

既然我们写了这么复杂的流程,我们就得确保这个流程是安全的。

7.1 环境变量管理

这是新手最容易踩的坑。千万不要在代码里写:

// backend/src/db.js
const dbUrl = 'mongodb://root:password123@localhost:27017/mydb'; 
// 这里的 password123 明文写在代码里,一旦推送到 GitHub,全宇宙都知道了。

正确做法

  1. .env.example 文件里写变量名,但不要写值。
  2. 在 CI 的 Secrets 里填入真实的值。
  3. 在代码里读取环境变量:process.env.MONGO_PASSWORD

7.2 分支保护

一旦你开启了 CI/CD,你就拥有了核按钮。但如果你允许任何人随便推 main 分支,那就麻烦了。

去 GitHub 仓库设置 -> Branches -> Add Rule。
选择 main 分支。
勾选:Require status checks to pass before merging
选择:Full Stack CI/CD Pipeline
这样,即使有人想强行提交代码,GitHub 也会说:“对不起,前面的测试没跑完,我不让你合代码。”

7.3 缓存优化

如果每次都要重新 npm install,你的 CI 流程会慢得像蜗牛。除了刚才提到的 cache: 'npm',对于 Node 项目,缓存 node_modules 也是个好办法。

第 8 节:故障排查——当 CI 失败时

写了这么多,万一失败了怎么办?别慌,我是专家,我会教你怎么看日志。

8.1 查看日志

GitHub Actions 的界面其实很直观。点进去某个 Job,你会看到一系列的步骤。
如果 npm test 失败了,点进去看 Detailed Output。通常错误信息都写在最上面。

8.2 常见错误

  1. Timeout error:通常是因为依赖安装太慢或者数据库连接太慢。尝试增加 timeout。
  2. Build failed:检查 package.json 里的 scripts 是否正确,检查代码里是否有语法错误。
  3. SSH Permission Denied:检查你的 SSH 密钥是否配置正确,是不是服务器上的 ~/.ssh/authorized_keys 文件没写对。

结语:拥抱自动化

好了,今天的讲座就到这里。

你现在拥有了:

  1. 一个像瑞士军刀一样整洁的代码仓库结构。
  2. 两套自动化的质量检测流程(前端和后端)。
  3. 一套自动化的 Docker 部署流水线。

当你下班回家,躺在床上,突然想到:“哎?昨天上线的那个功能是不是有个 Bug?” 你点开手机上的 GitHub App,看到绿色的 ✅ 指示灯,你会感到一种前所未有的安宁。因为你知道,哪怕半夜三更,你的代码也是健康的,你的服务器也是安全的。

这就是 CI/CD 的魔力。它不是一种负担,它是现代程序员的护身符。别再手搓部署脚本了,去配置你的 GitHub Actions 吧。当你看到绿色的“Success”弹窗时,你会发现,写代码原来可以这么快乐。

谢谢大家,我是你们的讲师,我们下次见。记得给项目点个 Star,如果不 Star,我会在梦里删掉你的 Dockerfile。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注