一次搞定前端部署方案的核心技术实践

美含 Dev 框架 阅读 939
赞 12 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

这个项目是个后台管理系统,用户量不算大,但部署频率挺高,基本一天三四次迭代。一开始我们用的是传统的 Nginx + 手动 scp 上传静态文件的方式,每次改完代码打包,再通过脚本推到服务器上。开始还挺顺,直到某天凌晨发版,我手一抖传错了 build 文件,直接导致线上白屏半小时——那一刻我就决定,不能再这么玩了。

一次搞定前端部署方案的核心技术实践

调研了一圈,CI/CD 方案当然首选 GitHub Actions,毕竟项目在 GitHub 上,不用额外搭 GitLab CI 或 Jenkins。而且团队人少,不想维护复杂的部署流水线。最后定了个轻量方案:push 到 main 分支自动构建,生成的产物通过 SSH 同步到目标服务器,配合 pm2 管理服务重启。听着简单,可真踩的坑一点不少。

最大的坑:SSH 连接不稳定

刚开始写的 workflow 是这样的:build 完之后用 appleboy/scp-action 把文件传上去,再用 appleboy/ssh-action 执行重启命令。本地测试没问题,一进 CI 就各种超时。最离谱的一次,scp 传了 80%,然后连接断了,workflow 显示成功,结果线上一半是新代码一半是旧的,页面直接报错。

查了半天才发现,GitHub Actions 的 runner 和我们的服务器网络有时候会抖,尤其是高峰期。后来加了重试机制也不靠谱,因为 scp 不支持断点续传。折腾了两天,最后换成了 rsync over ssh,自己写了个小脚本包装一下,至少能增量同步,失败了也能重来。

#!/usr/bin/env bash

# sync.sh
RSYNC_CMD="rsync -avz --delete -e 'ssh -o StrictHostKeyChecking=no -i /root/.ssh/deploy_key' dist/ user@server:/var/www/html"

retries=0
max_retries=3

while [ $retries -lt $max_retries ]; do
  if $RSYNC_CMD; then
    echo "Sync succeeded."
    exit 0
  else
    retries=$((retries + 1))
    echo "Sync failed. Attempt $retries of $max_retries."
    sleep 5
  fi
done

echo "All sync attempts failed."
exit 1

这个脚本跑在 action 的 container 里,私钥通过 secrets 注入,路径挂载进去。虽然不是最优解(比如没做指纹验证),但至少比原来稳定多了。这里注意我踩过好几次坑:StrictHostKeyChecking 必须关,否则第一次连不上;私钥权限要是 600,不然 ssh 直接拒绝读取。

核心代码就这几行

最终的 GitHub Actions 配置文件精简后长这样:

name: Deploy Frontend

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

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

      - name: Install dependencies and build
        run: |
          npm install
          npm run build

      - name: Upload artifact (for debugging)
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

      - name: Sync files via rsync
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /tmp && mkdir -p sync_script
            cat > sync.sh << 'EOF'
            # (上面那个 rsync 脚本内容)
            EOF
            chmod +x sync.sh
            ./sync.sh
          env:
            DIST_PATH: dist

其实关键就是把 rsync 脚本通过 here-doc 传到远程服务器去执行,避免中间网络中断导致文件不一致。虽然有点土,但亲测有效。另外 upload-artifact 那一步本来是调试用的,后来发现偶尔能帮上忙,就没删。

谁更灵活?谁更省事?

其实还有个方案是用 FTP 或直接 API 触发构建,但我们服务器没开 FTP,也不想为了部署多一个服务入口。也想过用 CDN + OSS 的方式,比如 build 完上传到七牛或 S3,再刷新缓存。理论上更稳定,但成本和复杂度都上去了,对我们这种小项目有点杀鸡用牛刀。

现在这套流程跑了几个月,大概发布了 70 多次,失败率从原来的 15% 降到了不到 3%。剩下的问题主要是服务器磁盘满导致同步失败,但这属于运维监控范畴了,不在本次解决范围。说到底,没有银弹,只有适合当前阶段的方案。

又踩坑了:环境变量污染

有次发版后登录接口 404,排查半天发现是 nginx 配置被覆盖了。原因是 rsync 命令用了 –delete,而我在本地 build 的时候不小心把 nginx.conf 也放进 dist 目录了……对,你没看错,是因为开发环境误引入了一个配置文件模板。

改法很简单,在 .gitignore 和 build 脚本里都加了清理逻辑:

// build.js
const fs = require('fs')
const path = require('path')

// 构建结束后清理敏感或多余文件
function cleanDist() {
  const distPath = path.resolve(__dirname, 'dist')
  const filesToRemove = ['nginx.conf', 'env.local', 'backup']

  filesToRemove.forEach(file => {
    const filePath = path.join(distPath, file)
    if (fs.existsSync(filePath)) {
      fs.rmSync(filePath, { recursive: true })
      console.log(Removed: ${file})
    }
  })
}

这事儿提醒我:自动化越强,破坏力也越大。现在每次上线前都会先看一眼 dist 目录结构,虽然麻烦,但心里踏实。

回顾与反思

这个部署方案谈不上优雅,也没有用 Kubernetes 或 Serverless 那种高大上的东西。但它解决了最实际的问题:让发布变得可预期、可重复、少出错。我现在敢在晚上十点发版,而不是非得等到凌晨三点没人用的时候。

做得好的地方:

  • 全流程自动化,除了 merge request 通过,不需要人工介入
  • 失败能快速重试,不会卡住整个流程
  • 产物保留一份在 GitHub artifacts,方便回滚对比

还能优化的点:

  • 应该加个 pre-sync 校验,比如检查 dist 是否包含 html 文件,避免传空目录
  • 目前没有版本标记,不知道当前线上是哪个 commit 构建的,考虑加个 version.json
  • 未来可以接入 webhook,发个钉钉通知告诉团队“已发布成功”

最后有个没完全解决的小问题:如果 build 过程中依赖了 jztheme.com 的某个接口返回数据,而那个接口临时不可用,会导致构建失败。目前只能等接口恢复后手动 rerun workflow。这不是大问题,一个月大概遇到一次,影响不大,所以暂时搁置了。

以上是我的踩坑总结

这套部署方案不是最优的,但足够简单、够用。如果你也在用 GitHub Actions 部署前端项目,希望这些经验能让你少熬两个夜。如果有更优雅的做法,比如怎么安全地处理 rsync 权限、或者如何优雅回滚,欢迎评论区交流。我已经快被部署搞怕了,能省一次心是一次。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论