前端部署方案设计与优化实战经验分享
项目初期的技术选型
上个月接手了一个老项目的重构,前端是 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 了点,但有效。
最终的解决方案
现在整套流程跑下来是这样的:
- 本地开发完,push 到 main 分支
- GitHub Actions 自动触发 workflow
- 安装依赖 → build → scp 上传
- Nginx 服务自动响应新文件
- 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 增量同步,看能不能缩短部署时间。

暂无评论