前端部署方案设计与优化实战经验分享

W″美菊 框架 阅读 2,865
赞 26 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月接手了一个老项目的重构,前端是 Vue 2 + Webpack 的架构,部署方式还是最原始的手动 build 后 scp 传到服务器。每次发版本都得小心翼翼,生怕漏了什么步骤。我第一次上线时就把 publicPath 配错了,页面白屏了快十分钟,运维在群里催命一样问“到底行不行”,尴尬得脚趾抠出三室一厅。

前端部署方案设计与优化实战经验分享

所以这次重构,第一件事就是把部署自动化搞起来。本来想直接上 CI/CD 流水线配 Jenkins,但团队小,没专职运维,Jenkins 维护成本太高,后来干脆用 GitHub Actions + Nginx 简单部署,本地提交完,自动打包、上传、重启服务,理想很丰满。

技术栈定了:Vue 3 + Vite,后端 API 地址是 https://jztheme.com/api,静态资源走同域。Vite 打包快,HMR 灵,适合我们这种小步快跑的迭代节奏。部署目标是让所有人 push 到 main 分支后,自动完成构建和发布,不再手动干预。

最大的坑:环境变量和路径问题

开始以为这事儿很简单,写个 workflow 脚本,build 完 scp 丢服务器就完事了。结果第一个 PR 合并后,CI 直接报错:error during build: Cannot resolve "@/utils/request"

查了半天发现是 Actions 环境里 Node 版本不对,本地是 18,workflow 默认是 16,有些 import alias 不兼容。改了 node-version 后又遇到新问题:build 出来的 JS 路径全是 /assets/xxx.js,但 Nginx 根目录下没有 assets 文件夹,404 一片。

这里注意我踩过好几次坑:Vite 的 base 配置不设的话,默认是 ‘/’,但在子路径部署或非根域名下就会出问题。我们这次是部署在二级路径 /app/ 下,所以必须改:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  base: '/app/',
  plugins: [vue()],
  build: {
    outDir: 'dist',
    assetsDir: 'static',
  },
  server: {
    port: 3000,
    open: true,
  }
})

这个 base: '/app/' 是关键,不然所有资源请求都会往根路径找,而我们的 Nginx 配置是:

location /app/ {
    alias /var/www/html/app/;
    try_files $uri $uri/ /app/index.html;
}

alias 配合 try_files,保证刷新页面也能命中 index.html。但刚开始没加 trailing slash,导致 /app 访问正常,/app/ 却 403,折腾了快一个小时才意识到是 Nginx 的路径匹配规则问题。

GitHub Actions 流水线怎么写

workflow 文件一开始抄了个网上的模板,用了 actions/upload-artifact,结果 artifact 只能存几天,没法部署到服务器。后来改用 SSH 部署,核心思路是:build 完把 dist 目录通过 ssh-copy 发到目标机器。

完整 workflow 如下:

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

on:
  push:
    branches: [ main ]

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

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

      - name: Install dependencies
        run: npm install

      - name: Build project
        run: npm run build

      - name: Deploy via SCP
        uses: appleboy/scp-action@v0.1.5
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.SSH_KEY }}
          source: "dist/*"
          target: "/var/www/html/app/"
          rm: true

这里有几个细节要注意:

  • rm: true 很重要,不然旧文件会残留,缓存问题严重
  • SSH_KEY 要用完整的私钥内容(包括 —–BEGIN OPENSSH PRIVATE KEY—–)
  • HOST 和 USER 放 secrets 里,别明文写

刚开始忘了 rm: true,有一次改了路由懒加载的 chunk 名字,旧的 js 还在服务器上,用户访问一直报 404,清缓存都解决不了,只能手动删。

还有个问题是缓存太狠

Vite 默认 build 会加 hash,比如 chunk-abc123.js,按理说更新后浏览器会重新拉。但我们首页是运营页,流量大,CDN 缓存了 index.html,导致用户一直访问旧版本。

最后解决方案是在 Nginx 层给 /app/index.html 加 no-cache:

location = /app/index.html {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    alias /var/www/html/app/index.html;
}

其他静态资源可以缓存一年,反正有 content hash。这个配置加上之后,至少用户打开的就是最新版了。

不过还是有个遗留问题:如果用户正在使用 App,我们发了新版本,他不会自动刷新。这块本来想上 SW 或 webpack 插件做版本检测,但时间紧就没搞。现在靠的是:每次发布在群里喊一嗓子“刷新一下页面”,low 是 low 了点,但有效。

最终的解决方案

现在整套流程跑下来是这样的:

  1. 本地开发完,push 到 main 分支
  2. GitHub Actions 自动触发 workflow
  3. 安装依赖 → build → scp 上传
  4. Nginx 服务自动响应新文件
  5. index.html 强制不缓存,用户下次打开就是新版

整个过程大概 3~4 分钟,比以前快多了。以前手动 build 再传,网络慢的时候要十几分钟,还容易出错。

中间也试过 rsync,但觉得没必要引入额外复杂度。scp 虽然慢一点,但稳定,出错也好排查。

回顾与反思

这套方案跑了一个月,总共自动部署了 27 次,失败 3 次,都是因为网络中断或者磁盘满了(对,服务器磁盘真满了,没人监控)。失败后得手动进服务器删旧文件,再重跑 workflow。

做得好的地方:

  • 完全去除了人工操作,减少失误
  • 环境变量和路径配置清晰,新人也能看懂
  • build 报错信息明确,基本能一眼定位

还能优化的:

  • 应该加个磁盘空间监控脚本,快满时告警
  • 可以考虑加 post-deploy hook,自动 reload nginx(现在是靠文件覆盖生效)
  • 版本降级不方便,目前只能 rollback 代码再重发,未来可以考虑多版本目录切换

还有一个没解决的小问题:偶尔 scp 会断连,导致部分文件没传全。现在靠的是 build 后加一个校验文件:

echo "BUILD_VERSION=$(date +%s)" > dist/BUILD_INFO

然后在服务器写个脚本检查是否存在 BUILD_INFO,作为部署完整性判断依据。虽然土,但有用。

以上是我的项目经验,希望对你有帮助

这方案不是最优的,比如没上 Kubernetes 或 Docker,也没用 CDN 全局分发,但对于中小型项目够用了。关键是:简单、可控、出问题能快速修。

如果你也在搞类似部署,建议先从 GitHub Actions + scp 开始,别一上来就想搞全自动化监控告警,容易把自己绕进去。

以上是我踩坑后的总结,有更优的实现方式欢迎评论区交流。下次打算试试 rsync 增量同步,看能不能缩短部署时间。

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

暂无评论