☰
Vue项目云服务器部署:Nginx静态托管与自动化发布
2026/11/2 4:05:07 网站建设 项目流程

1. 部署这件事,真正卡住人的从来不是技术

Vue项目写完之后往云服务器上放,是绝大多数前端从“会写页面”走向“能交东西”必须迈过去的一道坎。本地跑得好好的项目,一旦搬到云服务器上,白屏、404、刷新就挂、接口跨域、样式丢失,这些问题会一次性全冒出来。我自己第一次把 Vue 项目传到一台全新的云服务器时,折腾了差不多六个小时,最后发现原因只是base路径配错了加 Nginx 少写了一个try_files——而这两个错误在本地开发环境里永远不会暴露。

这篇内容讲的就是把 Vue 项目从本地仓库搬到云服务器并对外可访问的完整链路:构建产物是什么、服务器怎么准备、文件怎么传、Nginx 怎么配、HTTPS 怎么加、自动化怎么做、出了问题按什么顺序查。适合两类人:一类是刚写完第一个 Vue 项目、手里有一台云服务器但不知道下一步该做什么的同学;另一类是已经部署过几次、能跑起来但说不清每一步为什么这么做、遇到故障只能靠重启和重装的开发者。前一类读者能照着步骤直接抄,后一类读者能补上那些“能用但不知道为什么能用”的空白。

我下面讲的所有内容都基于静态站点部署这一条主线——也就是 Vue 项目打包成 HTML、CSS、JS 之后交给 Nginx 直接对外提供。这是目前中小型项目最常见、最省钱、故障面也最小的方案。至于 SSR、容器化、CDN 加速这些更复杂的形态,会在方案选型那一节里说清楚什么情况下才值得上。

2. 方案选型:先想清楚你的产物长什么样

2.1 三种常见部署形态的取舍对比

动手之前最重要的一步不是敲命令,而是先想清楚“我要把什么东西放到服务器上”。Vue 项目有三种主流落地方式,成本、复杂度、适合的场景差别很大,选错了后面每一步都是在还债。

部署形态服务器上跑的是什么资源占用运维复杂度适用场景
纯静态托管 + Nginx编译后的 dist 静态文件极低,1核1G 足够低,配一次基本不用管后台管理、官网、活动页、绝大多数业务前端
Node 服务托管(SSR)常驻的 Node 进程 + Nginx 反代中等,内存建议 2G 起中,要处理进程保活与重启需要 SEO 的内容型站点、首屏性能敏感的 C 端页面
容器化部署镜像 + 容器运行时较高,还要额外占磁盘高,需要理解镜像与编排多环境频繁发布、微服务架构下的前端

绝大多数人手里那个 Vue 项目,属于第一行。它的编译产物就是一堆静态文件,Nginx 这类静态 Web 服务器天生就是干这个的,稳定到几乎不需要维护。我见过不少团队为了“显得专业”硬上容器,结果每次发布都要多花十分钟构建镜像,最后发现收益全在仪式感上。

判断标准很简单:如果你的项目里没有必须跑在服务端的运行时逻辑(比如服务端渲染、服务端接口转发到内部网络、依赖 Node 环境做图片处理),那就老老实实走静态托管。别为了架构好看给自己加负担。

2.2 静态托管这套方案为什么故障面最小

静态托管的核心优势在于“没有状态”。Node 服务要考虑进程内存泄漏、依赖升级、端口占用、崩溃重启;容器要考虑镜像拉取失败、挂载卷权限、编排配置。而静态文件托管只有三件事可能出问题:文件没传对地方、Nginx 配置写错、域名和证书没弄好。这三件事都能通过一次配置长期稳定,之后几个月不碰它也不会自己坏掉。

另一个实际的好处是排查路径短。页面白屏的时候,如果是静态托管,你只需要确认三个点:文件在不在、Nginx 有没有读到、浏览器请求的路径对不对。三步之内基本能定位。换成 SSR,你还得先确认 Node 进程是不是活着、端口通不通、日志里有没有异常堆栈,排查链路长了一倍不止。

当然静态托管也有它的边界。如果你的项目依赖process.env在运行时读取配置、需要根据请求头动态渲染、或者要在服务端做鉴权拦截,那静态文件给不了你这些能力。这种时候要么把动态部分拆到后端接口,要么考虑第二种形态。我个人的偏好是把所有“运行时才需要知道的东西”都变成接口返回,前端只负责展示,这样构建产物永远是同一份,多环境部署时不需要为每个环境单独打包。

2.3 云服务器配置怎么估算,别拍脑袋买

配置选型这块最常见的浪费是“一上来就买 4核8G”,结果跑了半年 CPU 使用率没超过 5%。对纯静态托管来说,真正的瓶颈不是 CPU 和内存,而是带宽。

算一笔账:一个典型的 Vue 后台管理系统,构建后主包加上几个异步 chunk,gzip 压缩前大约 800KB 到 1.5MB,压缩后能降到 250KB 到 500KB。假设首屏需要加载 400KB 的压缩资源,服务器带宽是 1Mbps,理论下行速度约 128KB/s,那么首屏资源传输时间大约是 3 秒多,加上握手和渲染,用户感知到可交互大概在 4 到 5 秒。这个体验在弱网下会很难看。

如果升到 5Mbps,理论下行约 640KB/s,同样的资源 1 秒内传完,体验立刻不一样。所以对面向公网用户的站点,预算有限的情况下优先把钱花在带宽上,而不是堆 CPU 核心数。内部使用的后台管理系统(并发用户就几个),1Mbps 也够用,没必要多花钱。

内存方面,Nginx 处理静态文件的内存占用极低,1G 内存的实例跑单站点绰绰有余。但如果同一台服务器上还跑着数据库、缓存、或者你自己的后端服务,那就得另算,一般留出 512M 到 1G 给 Nginx 就够。CPU 同理,1核足够支撑相当可观的静态请求量。

一个容易被忽略的点:磁盘。系统盘给 40G 通常够,但如果你习惯把每次发布的产物都留一份做回滚,几个月下来能堆出好几个 G。我的做法是只保留最近五个版本,更早的自动清理,这个后面在自动化那一节会写成脚本。

3. 服务器环境准备:地基不平时后面全是坑

3.1 系统初始化与登录安全

拿到一台全新的云服务器,第一件事不是装 Node,而是把登录方式处理好。用密码登录、尤其是用简单密码并且开放 22 端口给所有地址,是很多小项目出问题的起点。我的习惯是三步:创建一个非 root 的普通用户、把公钥写到这个用户的authorized_keys里、然后关掉密码登录。

# 新建部署用的普通用户 adduser deployer usermod -aG sudo deployer # 切到该用户,写入自己的公钥 su - deployer mkdir -p ~/.ssh && chmod 700 ~/.ssh vi ~/.ssh/authorized_keys # 粘贴本地 ~/.ssh/id_rsa.pub 的内容 chmod 600 ~/.ssh/authorized_keys # 回到管理账户,调整 SSH 配置 sudo vi /etc/ssh/sshd_config # 修改或确认以下两行 # PasswordAuthentication no # PubkeyAuthentication yes sudo systemctl restart sshd

改完之后一定要保持当前会话不要断开,另开一个终端窗口测试能不能用新用户和新密钥登录。确认能登进来之后再关掉旧会话,否则一旦配置写错,你就只能去控制台用 VNC 救场了。这个习惯我建议一直保持:任何会改到远程登录配置的操作,都留一条活着的连接当后路。

另外操作系统层面还有两件小事值得顺手做:把时区改成自己习惯的时区(timedatectl set-timezone Asia/Shanghai),以及开启系统的自动安全更新。时区不对会导致日志时间全是乱的,排查问题时看着别扭;安全更新则是花五分钟换几个月的安心。

3.2 Node 运行时装到哪个版本

Vue 项目的构建工具对 Node 版本有硬要求。Vite 5 要求 Node 18 以上,Vite 6 要求 Node 18 或 20 以上,老一些的 webpack 4 项目在 Node 17 以上反而会报ERR_OSSL_EVP_UNSUPPORTED的加密算法错误。所以第一件事是翻一下项目的package.json里有没有engines字段,或者看看构建工具和依赖的官方说明要求的版本区间。

我更推荐用版本管理工具来装 Node,而不是直接用系统包管理器。系统自带的 Node 版本往往偏老,而且升级时会牵连一堆系统包。

# 用 nvm 管理 Node 版本 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc # 安装项目需要的版本(假设是 20) nvm install 20 nvm use 20 nvm alias default 20 # 验证 node -v && npm -v

装完之后顺手把 npm 的镜像源配一下,国内环境下装依赖能快不少,也避免构建阶段因为网络抖动反复失败。

npm config set registry https://registry.npmmirror.com

这里有个很多人踩过的坑:用sudo npm install -g装的全局包和 nvm 管理的 Node 是两套环境,全局包可能装到了系统 Node 的目录下,导致command not found。用 nvm 之后,全局安装一律不要加sudo,直接npm i -g xxx就行。

3.3 Nginx 安装与目录规划

Nginx 从系统包管理器装最省事,版本也够用。

sudo apt update sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx

装完之后在浏览器里访问服务器 IP,能看到 Nginx 默认欢迎页,说明 80 端口通了。如果看不到,先查安全组或者防火墙有没有放行 80,这是新手最常见的第一个坎——很多人会误以为是 Nginx 没启动,其实是被网络层的规则挡在外面。

目录规划这块我建议一开始就定好,不要随手往/var/www/html里塞。我的习惯是按项目分目录,每个项目一个文件夹,里面再按版本号分子目录:

/var/www/ └── my-vue-app/ ├── releases/ │ ├── 20240601-1530/ │ ├── 20240608-0920/ │ └── 20240615-1845/ └── current -> releases/20240615-1845

current是一个软链接,永远指向当前生效的版本。Nginx 配置里 root 指向/var/www/my-vue-app/current。这样发布新版本就是“把新目录建好、把软链接指过去”,回滚就是把链接指回上一个版本,全程不需要改 Nginx 配置,也不需要重启服务。这个设计在真正需要紧急回滚的时候能救命。

sudo mkdir -p /var/www/my-vue-app/releases sudo chown -R deployer:deployer /var/www/my-vue-app

4. 构建阶段:产物对不对,决定了后面所有事

4.1 base 路径配错是白屏的头号原因

项目要部署到域名根目录(https://example.com/),base用默认的/就行。但如果要部署到子目录(https://example.com/admin/),就必须把base改成/admin/,否则打包出来的 HTML 里所有资源引用路径都是/assets/xxx.js,浏览器会去根目录找,自然全部 404,页面一片空白。

Vite 项目的配置长这样:

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ // 部署在根目录就写 '/',部署在子目录就写 '/admin/' base: process.env.NODE_ENV === 'production' ? '/admin/' : '/', plugins: [vue()], build: { outDir: 'dist', assetsDir: 'assets', }, })

Vue CLI(webpack)项目对应的是publicPath:

// vue.config.js module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/admin/' : '/', outputDir: 'dist', }

注意base的值必须以斜杠开头、以斜杠结尾,写成admin或者/admin都会出问题。判断有没有配对的土办法:打包之后打开dist/index.html,看里面引用资源的路径,如果是以/开头的绝对路径且跟你的部署位置一致,那就对了。这一步花三十秒,能省掉后面半小时的抓瞎。

4.2 环境变量与多环境打包

前端项目的环境变量有个基本事实必须认清:打包之后,所有环境变量都会被静态替换成字面量写进 JS 文件里。这意味着你写在.env.production里的任何内容,都能被用户在浏览器里看到。所以 API 密钥、数据库连接串这类东西绝对不能放前端环境变量,只能放在服务端。

Vite 里只有以VITE_开头的变量才会被注入到客户端代码,这是个安全设计,防止你误把 Node 环境的变量暴露出去。

# .env.production VITE_API_BASE_URL=https://api.example.com VITE_APP_TITLE=运营后台
// 代码里这样读 const baseURL = import.meta.env.VITE_API_BASE_URL

Vue CLI 对应的是VUE_APP_前缀,通过process.env.VUE_APP_XXX读取。多环境打包就是准备多个.env文件,构建时通过--mode参数指定:

npm run build -- --mode staging

我见过一个很典型的事故:某个项目把测试环境的接口地址硬编码在代码里,上线时忘了改,结果生产用户的数据全打到了测试库。用环境变量文件之后,这类问题从“靠人记”变成了“靠配置”,出错概率直线下降。

4.3 产物瘦身与 sourcemap 的取舍

构建完之后先看一眼dist目录有多大。如果主包超过 1MB(未压缩),基本可以确定有优化空间。几个见效最快的动作:

  • 关掉生产环境的 sourcemap。build.sourcemap默认是false,但如果团队为了排查线上问题打开了它,产物体积会翻好几倍。如果确实需要,改成'hidden',生成但不让浏览器自动加载,出问题时手动上传给错误监控工具。
  • 检查有没有整包引入的 UI 组件库。按需引入能把体积砍掉一大半,具体看组件库文档的配置方式。
  • 把不常变的第三方依赖单独拆包。Vite 里用build.rollupOptions.output.manualChunks配置,好处是业务代码更新时用户不需要重新下载整个 vendor 包,缓存命中率会高很多。
build: { rollupOptions: { output: { manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'ui-vendor': ['element-plus'], }, }, }, },

一个经验值:对于一个中型的后台管理系统,合理的产物体积是 gzip 后主包 200KB 以内、总资源 1MB 以内。超出这个范围先别急着优化,先确认是不是把整个组件库或者图片资源全打进包里了。

5. 文件上传:几种方式的实测对比

5.1 scp 与 rsync 的实际差异

最直接的方式是scp,把整个dist目录推到服务器上:

scp -r dist/* deployer@服务器IP:/var/www/my-vue-app/releases/20240601-1530/

简单,但每次都是全量传输。一个几百 KB 的项目问题不大,如果是带大量图片、字体文件的项目,反复全量传会很难受。

rsync只传差异部分,第二次之后速度快很多,而且支持--delete清理目标目录里多余的文件——后者很重要,因为打包产物的文件名带哈希,旧文件不删会越积越多。

rsync -avz --delete \ dist/ \ deployer@服务器IP:/var/www/my-vue-app/releases/20240601-1530/

注意dist/后面那个斜杠不能省。带斜杠表示“把 dist 里的内容同步过去”,不带斜杠表示“把 dist 这个目录本身同步过去”,结果会差一层目录,很容易搞错。

两种方式的对比:

方式传输效率是否支持增量额外依赖适合场景
scp一般否,每次全量无,系统自带首次部署、小体积项目、临时传文件
rsync高是服务器需装 rsync日常发布、大体积项目、频繁迭代

服务器上装 rsync 就一行命令,装完之后我基本不再用 scp 传构建产物了。

5.2 版本控制拉取的思路与边界

另一种做法是在服务器上git pull,然后本地构建或者服务器构建。这种方式在纯静态项目上我不太推荐,原因有两个。

第一,构建产物不应该进 Git 仓库。把dist提交上去会让仓库迅速膨胀,而且每次发布都产生一堆“构建产物更新”的无意义提交。第二,如果改成在服务器上拉源码再构建,服务器就得装全套 Node 依赖,构建过程还会占用服务器资源,遇到内存小的实例直接 OOM 中断,很常见。

我的用法是:源码用 Git 管理,构建在本地或者流水线里做,服务器上只放构建产物。两边各司其职,服务器永远保持干净,不需要 Node 环境,也不需要装一堆开发依赖。

5.3 上传之后的权限处理

文件传上去之后如果权限不对,Nginx 会返回 403。原因是 Nginx 的工作进程通常以www-data或nginx用户运行,需要对这些文件有读权限,而且路径上每一层目录都需要有执行权限(能进入目录)。

# 目录给 755,文件给 644 sudo find /var/www/my-vue-app/current -type d -exec chmod 755 {} \; sudo find /var/www/my-vue-app/current -type f -exec chmod 644 {} \; # 确认属主是部署用户,Nginx 只需要读权限 sudo chown -R deployer:deployer /var/www/my-vue-app/current

千万别图省事直接chmod -R 777。这会让任何用户都能改写你的前端文件,一旦服务器上有其他低权限进程被利用,你的站点内容就能被随意替换。755 和 644 已经足够 Nginx 正常读取了。

6. Nginx 配置:让页面真正跑起来

6.1 最小可用配置逐行拆解

配置文件放在/etc/nginx/sites-available/下,然后软链到sites-enabled/,这是 Debian 系的标准做法,好处是随时可以禁用某个站点而不删文件。

# /etc/nginx/sites-available/my-vue-app server { listen 80; server_name example.com www.example.com; root /var/www/my-vue-app/current; index index.html; access_log /var/log/nginx/my-vue-app.access.log; error_log /var/log/nginx/my-vue-app.error.log; location / { try_files $uri $uri/ /index.html; } }

逐行说重点。listen 80监听 HTTP 端口;server_name写域名,没域名的话写服务器 IP 或者_。root指向前面规划的current软链接,这样发布新版本时改软链接即可,配置不用动。

try_files $uri $uri/ /index.html这一行是整个配置里最关键的。它的意思是:先按请求的路径找对应文件,找不到就找同名目录,还找不到就返回index.html。这一行解决了前端 history 路由的刷新 404 问题,下面单独说。

access_log和error_log分开写,方便以后按站点排查。默认日志会混在一起,站点多了就没法看了。

sudo ln -s /etc/nginx/sites-available/my-vue-app /etc/nginx/sites-enabled/ sudo nginx -t # 先测配置语法 sudo systemctl reload nginx

nginx -t这一步务必养成习惯。配置有语法错误时 reload 会失败但旧进程还在跑,你会以为改动生效了,实际访问的还是老配置,白白浪费排查时间。

6.2 history 路由刷新就 404 的根因

Vue Router 默认用 history 模式,URL 长这样:https://example.com/user/123。这个地址在浏览器里看起来正常,但用户一按 F5,浏览器就会向服务器请求/user/123这个路径。服务器上没有这个文件,也没这个目录,Nginx 直接返回 404。

开发环境不会出这个问题,因为 Vite 和 webpack-dev-server 内置了 fallback,把所有未匹配的路径都转给index.html。生产环境需要你自己在 Nginx 里做同样的事,就是那个try_files。

这里有个细节值得说清楚:try_files的最后一项如果写成/index.html(带斜杠),Nginx 会做一次内部重定向,重新走一遍 location 匹配;如果写成index.html(不带斜杠),是相对于 root 的相对路径查找,不会重新匹配 location。两种写法大多数场景下效果一样,但如果你在location /之外还配了针对index.html的特殊规则(比如禁止缓存),带斜杠的写法才会让那些规则生效。我一般用/index.html。

如果站点部署在子目录,try_files的最后一项也要跟着改:

location /admin/ { try_files $uri $uri/ /admin/index.html; }

这个对应关系忘了改,表现就是子目录下刷新 404。

6.3 缓存策略:哪些能长缓存,哪些一秒都不能

前端资源分两类,缓存策略完全相反。

带哈希的文件(index-a1b2c3.js)内容变了文件名就变,可以放心设置超长缓存,一年都行。这样用户第二次访问时直接从本地读,不产生任何请求。

index.html绝对不能长缓存。它是整个应用的入口,里面引用了哪些 JS 文件全靠它。如果它被缓存了,用户拿到的还是旧入口,会去加载已经被删掉的旧 JS 文件,结果就是白屏。这个坑非常经典,表现是“改了代码发布了,自己电脑上刷新没变化,清缓存就好了,但用户不会清缓存”。

# 带哈希的静态资源,长缓存 location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # 入口文件不缓存 location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; expires 0; }

immutable这个指令告诉浏览器“这个文件在有效期内绝对不会变,不用发请求验证”,配合内容哈希使用是安全的,能省掉一次条件请求的往返。

6.4 gzip 压缩与接口反向代理

文本资源开启 gzip 之后体积通常能降到原来的三成左右,对首屏速度的影响立竿见影。放在http块或者server块里都行:

gzip on; gzip_vary on; gzip_min_length 1k; gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript application/xml text/xml image/svg+xml;

gzip_min_length 1k是为了避免压缩小文件反而变大(压缩有固定头部开销)。gzip_comp_level 6是压缩比和 CPU 消耗的平衡点,调到 9 收益很小但 CPU 占用明显上升,不划算。注意图片和字体不要放进gzip_types,它们本身已经是压缩格式,再压一遍只会浪费 CPU。

接口这块,如果前端和后端在同一个域名下,最省事的方式是用 Nginx 反代,把/api/开头的请求转给后端服务:

location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }

注意proxy_pass结尾的斜杠。写http://127.0.0.1:8080/会把/api/这一段剥掉再转发,后端收到的是/user/list;不写斜杠则原样转发,后端收到/api/user/list。这两种行为差异很大,配错了就是 404,而且看日志很容易看漏。

走同域反代还有个附带好处:跨域问题直接消失了。浏览器只认域名,前后端同域名同端口,浏览器的同源策略根本不触发,也就不需要后端配一堆 CORS 头,更不用在前端搞代理转发。这是我在小项目上最推荐的做法。

7. HTTPS:不只是为了那个小锁图标

7.1 证书获取与自动续期

现在的免费证书签发已经很成熟,用certbot一条命令就能搞定申请和配置,它还会自动帮你改 Nginx 配置。

sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com

执行过程中会让你选是否强制跳转 HTTPS,选是就行。证书有效期 90 天,certbot安装时会自动注册一个定时任务做续期,可以手动验证一下续期流程是否正常:

sudo certbot renew --dry-run

这个 dry-run 建议每次都跑一遍。我遇到过定时任务因为路径问题没执行成功、证书到期当天站点直接无法访问的情况——浏览器对证书过期的处理是直接拦截,用户看到的是一个红色警告页,比 404 还糟糕。

续期失败最常见的原因是 80 端口的验证路径被挡住。如果你在 Nginx 里配了全站强制跳转 HTTPS,记得放行/.well-known/acme-challenge/这个路径,否则续期时验证请求会被 301 到 HTTPS,验证就失败了。

7.2 强制跳转与几个值得加的安全响应头

强制跳转的配置很简单,但要注意别把验证路径也跳了:

server { listen 80; server_name example.com www.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } }

HTTPS 的 server 块里可以顺手加几个响应头,成本很低:

add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Referrer-Policy strict-origin-when-cross-origin;

nosniff阻止浏览器猜测 MIME 类型,X-Frame-Options防止页面被别的站点用 iframe 嵌套(点击劫持),Referrer-Policy控制跳转时携带的来源信息。这三个头不影响业务逻辑,但能挡掉一批低成本的攻击手法。

8. 自动化:把每次发布从十分钟压到一分钟

8.1 一条能用的部署脚本

手动传文件、改软链接、reload 这套流程跑三四次就会烦,而且每次都有漏步骤的风险。写成一个脚本,本地执行一条命令完成全部动作:

#!/usr/bin/env bash set -euo pipefail SERVER="deployer@服务器IP" APP_DIR="/var/www/my-vue-app" STAMP=$(date +%Y%m%d-%H%M%S) TARGET="$APP_DIR/releases/$STAMP" echo "==> 构建" npm run build echo "==> 上传到 $TARGET" ssh "$SERVER" "mkdir -p $TARGET" rsync -avz --delete dist/ "$SERVER:$TARGET/" echo "==> 切换软链接" ssh "$SERVER" " chmod -R 755 $TARGET ln -sfn $TARGET $APP_DIR/current sudo nginx -t && sudo systemctl reload nginx " echo "==> 清理旧版本,保留最近 5 个" ssh "$SERVER" " cd $APP_DIR/releases ls -1dt */ | tail -n +6 | xargs -r rm -rf " echo "==> 完成,版本 $STAMP"

set -euo pipefail这三项很关键:任何一步失败立即中止,不会出现“构建失败但还把空目录传上去”的情况。ln -sfn的-n参数是为了防止目标已经是软链接时被当成目录处理,导致链接套链接。

脚本里的清理逻辑用的是“按修改时间倒序,从第 6 个开始删”。这比按文件名排序稳妥,因为时间戳格式本身是可排序的,但万一有人手工建过目录,按时间判断更符合“保留最近的”这个真实意图。

8.2 接入流水线后的触发方式

脚本能跑之后,往前推一步就是交给代码托管平台的流水线。触发条件一般是推送到主分支或者打标签。流水线里的步骤和脚本基本一致,区别在于密钥要用平台提供的加密变量存,不要硬编码在配置文件里。

一个常见的坑是流水线环境的 Node 版本和本地不一致,导致构建产物行为不同。解决办法是在项目根目录放一个.nvmrc写明版本号,流水线里读取这个文件来装对应版本,本地和线上就对齐了。

另一个坑是流水线里的rsync可能没装。要么在步骤里先装,要么改用平台的部署插件,别假设环境里什么都有。

8.3 回滚要能在三十秒内完成

发布出问题的时候,第一优先级不是找 bug,而是先恢复服务。前面用软链接的设计就是为了这个:

# 看现有版本 ls -1dt /var/www/my-vue-app/releases/*/ # 回退到上一个 ln -sfn /var/www/my-vue-app/releases/20240608-0920 /var/www/my-vue-app/current

不需要重新构建,不需要重启 Nginx(软链接指向的文件变了,Nginx 下次读取时自然拿到新内容)。整个过程十几秒。等线上稳住了,再慢慢查问题出在哪,心态会完全不一样。

有个细节容易忽略:浏览器里可能还缓存着出问题那版index.html。回滚之后如果自己测试还是旧页面,先用无痕窗口验证,别被自己的缓存骗了。

9. 问题排查:按顺序查,别乱试

9.1 白屏、404、502 三类故障的定位顺序

白屏是最常见的。打开浏览器开发者工具,看 Network 面板里红色的请求。如果 JS 和 CSS 全是 404,八成是base路径配错;如果资源 200 但控制台报错,可能是某个运行时错误或者环境变量没注入;如果 HTML 请求本身就是 404,那是 Nginx 的 root 配错了或者文件没传上去。

刷新 404(进入首页正常,点进某个路由再刷新就挂)几乎百分百是try_files没配或者配错。检查 Nginx 配置里有没有那一行,以及路径前缀和base是否一致。

502 Bad Gateway只会在反代场景出现,意思是 Nginx 连不上后端。检查后端进程是不是活着、端口对不对、proxy_pass的地址有没有写错。这个错误跟前端代码完全无关,别在前端改动上浪费时间。

9.2 常见问题速查表

现象最可能的原因快速验证方式处理办法
访问 IP 打不开安全组/防火墙没放行 80本地curl -I http://IP看是否超时放行端口
页面白屏,资源全 404base路径与实际部署位置不符打开dist/index.html看资源引用路径改base重新构建
刷新页面 404缺少 history 路由 fallback看 Nginx 配置有无try_files补上try_files $uri $uri/ /index.html
403 Forbidden文件权限不足或目录缺执行权限看 error.log 里的 Permission denied目录 755,文件 644
改了代码线上没变index.html被浏览器缓存无痕窗口访问对比给 index.html 加 no-cache
接口报跨域前后端域名不同且未配 CORS看 Network 里的 CORS 报错走 Nginx 同域反代
502 Bad Gateway后端进程挂了或端口错curl 127.0.0.1:端口试一下重启后端或修proxy_pass
上传大文件失败Nginx 客户端体积限制看 error.log 有无 413调client_max_body_size
证书到期打不开自动续期任务失败certbot renew --dry-run修验证路径后手动续期

9.3 几个我实际踩过的坑

第一个是大小写敏感。开发在本地文件系统上不区分大小写,import Header from './header.vue'这种写法本地能跑,Linux 服务器上就找不到文件,构建阶段直接报错。这类问题在构建时就暴露还算幸运,更麻烦的是某些运行时的图片路径拼错,本地正常、线上图裂。解决办法是统一规范文件名全小写加连字符,别用驼峰。

第二个是软链接和 Nginx 的 root 安全限制。如果你的站点目录是软链接,而nginx.conf里的user指令权限不对,Nginx 可能会拒绝跟随符号链接,报 403。这个现象很迷惑,因为文件明明存在、权限明明是对的。排查方式是看 error.log 里有没有is forbidden之类的记录,或者临时把 root 换成真实路径测试对比。

第三个是构建产物里的绝对路径写死在 CSS 里。某些 UI 库的样式里带url(/fonts/xxx.woff)这种绝对路径,子目录部署时全部失效。这种问题只能靠改base之后完整走一遍线上环境验证,本地开发环境发现不了。

第四个是时区导致的时间显示错误。服务器时区是 UTC,前端直接展示后端返回的时间戳,用户看到的时间比实际早了八小时。这类问题上线后很久才会被发现,因为开发阶段大家都在同一个时区。服务器时区统一、接口统一返回带时区的标准格式,能避免大部分麻烦。

第五个是发布时忘记 reload 但以为生效了。其实改软链接这种操作不需要 reload,但如果是改了 Nginx 配置文件就必须 reload。我一度把这两件事搞混,改了配置死活不生效,最后发现是忘了systemctl reload nginx。现在的习惯是:只要碰了/etc/nginx/下的文件,敲完就立刻补一条nginx -t && systemctl reload nginx,形成肌肉记忆。

整套流程跑顺之后,一次发布从构建到线上生效大概一分半钟,其中大部分时间花在npm run build上。我自己的习惯是把这个脚本放在项目根目录,文件名固定,团队里谁来发布都是同一条命令,不需要口口相传步骤。部署这件事的价值不在于你配得多花哨,而在于它足够简单、足够可重复,简单到不会出错,可重复到换个人来也能一模一样地跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询