把Vue项目从本地跑到线上,看起来就三步:打包、传文件、配一个Web服务器。但很多前端同学在第三步栽跟头——项目传到服务器上,双击index.html能打开,一刷新就404;或者接口全部飘红,控制台一片报错。这些问题十有八九出在Nginx反向代理配置上。Nginx反向代理部署前端Vue项目,核心就两件事:第一,把打包出来的静态文件用正确的路径交给浏览器;第二,把前端发出的 /api 请求转给后端服务,解决跨域、隐藏服务地址,同时让前后端保持同源。这篇文章会从“为什么需要反向代理”讲到“配置文件的每一行怎么理解”,再到“完整部署的每一步命令”和“上线后常见问题的排查套路”。不管你是第一次部署个人项目的前端新人,还是在公司环境里上线业务系统的工程师,这套经验都能直接用。
1. 为什么前端部署绕不开Nginx反向代理
1.1 打包后的Vue项目,离“能上线”还差一步
运行npm run build之后,你会得到一个 dist 目录,里面通常有一个 index.html,加一个 assets 目录,js、css、图片都放在里面,文件名一般带 hash。Vue 是单页应用,整个应用只有一个真实的 HTML 文件,后续路由跳转全靠 JS 在浏览器里渲染。也就是说,浏览器要访问你的项目,服务器必须能提供 index.html,以及它引用的那批资源文件。
问题在于,这个 index.html 里引用的路径是绝对路径还是相对路径,直接决定线上能不能打开。用默认配置构建时,它通常是/assets/index-xxxx.js这种根路径引法。浏览器访问 example.com 时,会去 example.com/assets/index-xxxx.js 下载 JS。如果服务器上没把静态文件放在对应目录,或者 Nginx 的 root 配置不对,资源直接404,页面就白屏。所以部署的第一件事,就是搞清楚:Web服务器是怎么把一个 URL 映射到磁盘上某个文件的。Nginx 做这件事极其擅长,它就像一个高速公路收费站,每条 URL 都能被精准导向正确的文件或后端服务。
1.2 反向代理解决的不只是跨域
开发环境里,Vue 项目跑在 vite 或 webpack-dev-server 上,它内部已经帮你处理好了静态服务和开发代理。接口请求比如/api/user/list,在vue.config.js里配了 proxy 之后,dev server 会把它转发到后端的http://localhost:9090。浏览器端看到的始终是http://localhost:5173/api/user/list这种同源请求,压根没有跨域的概念。
但一到生产环境,静态文件不会自己跑。很多人把 dist 目录上传后直接扔给 Nginx,发现接口请求全挂了,因为浏览器请求http://your-server/api/user/list,Nginx 如果真的去磁盘上找api/user/list,当然找不到。这时候就需要反向代理:把/api/这个路径转发给后端服务,后端处理完再把响应通过 Nginx 返回给浏览器。浏览器全程不知道后端的真实地址,请求和响应都经过 Nginx 中转,同源问题自然消失。
反向代理的价值还不止于此。它能把后端服务的真实端口、IP、内网结构全部藏在幕后,外部只能看到一个入口。遇到多台后端实例时,还能做简单的负载均衡。对前端来说,最直观的收获是:不用再在前端代码里写死跨域地址,所有请求统一走同源相对路径,部署时只需要改 Nginx 配置,代码一行都不用动。
1.3 横向对比:Nginx、Apache、Caddy、Node
可能有人会问,用 Node 自己写个静态服务器不行吗?用 Apache 不行吗?都可以,但实际选型时各有取舍。我整理过一份简单对比,放这里给大家参考:
| 方案 | 静态文件性能 | 反向代理配置 | 内存占用 | 生态与文档 | 适合场景 |
|---|---|---|---|---|---|
| Nginx | 高 | 灵活,功能全 | 低 | 极其丰富 | 绝大多数生产环境 |
| Apache | 中 | 可用,但配置偏重 | 中高 | 丰富 | 老牌Linux环境 |
| Caddy | 中高 | 配置极简,自动HTTPS | 低 | 增长中 | 个人项目、快速上线 |
| Node自写 | 中 | 需要自己实现 | 视实现而定 | 一般 | 极简内部工具 |
Nginx 胜在均衡:静态文件性能好,反向代理功能成熟,配置语法虽然不算友好,但资料实在太多了。面试里也经常问 Nginx 相关的问题,比如反向代理原理、location 匹配顺序、跨域解决方案,这些都是前端进阶绕不开的知识点。所以不管从项目上线还是职业发展角度,Nginx 都值得花时间搞明白。
2. 理解这几组Nginx概念,配置不再靠猜
2.1 配置文件是分层的,动手前先看清层次
Nginx 装好之后,主配置文件一般在/etc/nginx/nginx.conf,里面通过 include 引入了一堆子配置。Ubuntu 系的通常还有/etc/nginx/sites-available和/etc/nginx/sites-enabled,一个放可用配置,一个放启用配置,实际生效的是 enabled 里的软链接。CentOS 系更习惯直接用/etc/nginx/conf.d/下的 .conf 文件。
Nginx 配置是分层的,从外到内大致是:main 层、events 层、http 块、server 块、location 块。main 层管 worker 进程数量、日志级别这些全局设置;http 块是所有虚拟主机的公共区域,gzip、超时时间、日志格式经常写在这里;server 块就是一个虚拟主机,负责监听某个端口或域名;location 块则是在一个 server 里按 URL 路径细分处理逻辑。
前端新人最容易搞混的是 server 和 location 的关系。我打一个比方:server 是一栋楼的门牌号,location 是大楼里各楼层的分诊台。请求到达 80 端口时,Nginx 先根据 server_name 和端口决定进哪栋楼,然后根据 URL 前缀决定去哪个分诊台处理。理解这个层级,后面配置看到一堆花括号就不会晕了。
2.2 location匹配优先级才是请求分流的根本
部署 Vue 项目时,90% 的场景只需要两个 location:一个托管前端页面,一个代理后端接口。但很多人一上来就写一堆 location,结果某个请求走进了错误的 location,出现各种匪夷所思的 404。
Nginx location 匹配优先级从高到低是这样的:
=精确匹配^~前缀匹配,命中后不再检查正则~或~*正则匹配,区分大小写与不区分/普通前缀匹配
举个例子,如果同时存在location ^~ /assets/和location ~ \.js$,请求/assets/app.js会按^~规则走,不再管后面的正则。反过来,如果只有location ~ \.js$和location /,那/assets/app.js就会走进正则块。
实际部署中,如果手误把location /assets/写成了location ~ /assets/,那么所有以 /assets/ 开头的请求都会优先匹配到正则块。如果那个块没有配置正确的 root,静态资源就全挂了,页面看起来就是“样式丢失、布局错乱”。这类问题排查起来很费劲,因为 Nginx 的报错可能很隐晦,但只要掌握优先级规则,一眼就能看出问题所在。
2.3 proxy_pass的斜杠之争:剥前缀与不剥前缀
这是反向代理配置里最经典的坑,没有之一。看两段配置:
location /api/ { proxy_pass http://127.0.0.1:8080; }请求/api/user/list到后端时,后端收到的是/api/user/list。
location /api/ { proxy_pass http://127.0.0.1:8080/; }请求/api/user/list到后端时,后端收到的是/user/list。
区别就在proxy_pass末尾这个斜杠。Nginx 的规则是:如果 proxy_pass 后面带了 URI,也就是有/或者完整路径,那么 location 匹配到的前缀会被替换成这个 URI;如果不带 URI,原路径原封不动转发。
这个坑决定了后端接口的匹配方式。后端如果所有接口已经挂在/api下,那 proxy_pass 末尾不能加斜杠;如果后端接口本身没有/api前缀,就必须加斜杠把前缀剥掉。配错的结果通常就是接口404,或者后端收到一堆奇怪的路径。我每次写配置都会专门确认一遍后端网关的路径规则,宁可多花一分钟,也不想上线后抓耳挠腮。
2.4 try_files是history路由刷新404的唯一解药
Vue Router 默认用 history 模式,地址栏里是 example.com/user/123 这样的真实路径。但这个路径在服务器磁盘上根本不存在,因为 SPA 只有一个 index.html。如果 Nginx 只配置了 root 和 index,没有额外处理,浏览器刷新/user/123时 Nginx 去磁盘找这个文件,找不到,直接返回404。
try_files 就是为这个场景设计的:
location / { root /var/www/my-vue; index index.html; try_files $uri $uri/ /index.html; }这行的意思是:请求进来后,先按 URL 找文件($uri),找不到就找目录($uri/),还是找不到,就统一返回 /index.html。浏览器拿到 index.html 之后,Vue Router 读取当前URL,匹配到 user 详情路由,页面正常渲染。这就是 “history 模式刷新404” 的标准解药。
如果你用的是 hash 模式,URL 带#,跳转时不会真的请求服务器路径,可以不配 try_files。但实际部署我还是建议统一配上,因为 history 模式更干净,也更符合多数项目的路由规划。
3. 手把手完成Vue项目部署全流程
3.1 打包前最后确认的三个关键参数
很多人打包失败,不是命令不对,而是构建前的配置没弄对。我总结成三个必须确认的点:
第一,路由模式。开发完成后确认项目用的是 history 还是 hash。history 模式需要 Nginx 配 try_files,这个前面已经讲过,千万别漏。
第二,publicPath 或 base。Vue CLI 项目叫 publicPath,Vite 项目叫 base。部署在域名根路径时保持/即可;部署在子路径,比如 https://example.com/tools/ ,就要设置成/tools/。这个配置决定 index.html 里脚本和样式资源的前缀,配错的结果就是页面白屏或者布局异常。
第三,生产环境接口地址。在.env.production里,如果后端接口统一走/api前缀,那么前端代码里的请求地址就应该写成相对路径/api/xxx,而不是写死某个 IP。这样打包产物里就不会有任何跨域地址,上线后只需要让 Nginx 把/api转发到后端即可。一旦前端代码写死了跨域地址,Nginx 反代也帮不上忙,只能重新打包。
3.2 构建产物检查:先自检再上线
执行npm run build之后,进入 dist 目录,打开 index.html,重点看<script>标签的 src 路径。
- 根路径部署,预期是
/assets/index-xxxx.js。 - 子路径部署,预期是
/tools/assets/index-xxxx.js。
如果发现路径不对,回去改 publicPath 或 base,重新构建。这一步省掉的话,等上了服务器再发现资源404,来回传文件特别浪费时间。
另外一个容易忽略的细节:dist 目录里不要有多余的 map 文件。生产环境构建默认不生成 sourcemap,如果某些旧项目配置过productionSourceMap: true,记得关掉。sourcemap 在线上的作用很小,反而会暴露源码,还增加部署体积。
3.3 上传与安装Nginx的实操命令
上传 dist 内容到服务器,我推荐用 rsync。命令大概是:
rsync -avz --delete dist/ root@your-server:/var/www/my-vue/注意dist/后面有斜杠,表示把 dist 目录里的内容同步到目标目录,而不是把 dist 目录本身打包放进去。--delete会删除目标目录里多余的旧文件,做全量发布时很有用,能避免旧文件残留引发路径混淆。
Ubuntu 系统安装 Nginx 很简单:
sudo apt update sudo apt install nginx -y sudo systemctl enable --now nginxCentOS 系的话是sudo yum install nginx,安装后配置文件路径和默认站点目录略有不同。Ubuntu 的默认站点根目录是/var/www/html,Nginx 默认配置会在/etc/nginx/sites-available/default里定义。实际部署时,我更建议新建一个独立的配置文件,不要直接改默认配置,这样项目隔离清晰,出问题也容易回滚。
3.4 可直接抄作业的Nginx完整配置
下面这份配置是我平时项目上线用的模板,支持静态文件托管和 API 反向代理,注释也写在里面了:
server { listen 80; server_name example.com; root /var/www/my-vue; index index.html; # 前端路由兜底,解决 history 模式刷新 404 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 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_connect_timeout 30s; proxy_read_timeout 60s; } # 静态资源缓存,带 hash 的文件可以放心缓存 location /assets/ { expires 7d; add_header Cache-Control "public, no-transform"; } }逐段说明一下:
root /var/www/my-vue;指向打包产物所在目录,可以按自己的路径改。location /里的try_files解决刷新404,必须配。location /api/的proxy_pass我用了尾部斜杠,是因为后端接口没有/api前缀。如果你的后端接口本身就带/api,请把末尾斜杠去掉,否则路径会被剥掉,后端反而匹配不上。proxy_set_header这几行让后端能拿到真实客户端 IP、协议和 Host。如果不配,后端看到的所有请求都来自 127.0.0.1,日志和风控都会受影响。- 超时时间根据业务调整。如果是文件上传接口,
proxy_read_timeout要适当加大。
配置写好后,依次执行:
nginx -t nginx -s reloadnginx -t是测试语法,报错的话会提示具体行号。改完配置先测试再 reload,这个习惯能规避掉 90% 的在线事故。
3.5 部署完成后的验证步骤
配置生效后,不要急着关终端,验证一遍再走。
首先访问http://你的域名/,页面正常打开,Network 面板里没有红色请求。然后随便刷新一个前端路由地址,比如/user/123,页面仍然正常,没有404。接着看接口请求,比如/api/user/list返回200,数据正常。如果是登录功能,再测一次登录,确认 Cookie 能正常写入和携带。
如果这几步都通过,部署基本没问题。剩下就是观察日志,确认没有持续报错,再离开服务器。
4. 上线后的坑位地图与日常维护
4.1 高频故障速查表
这是我平时帮同事排查问题用的速查表,遇到类似现象可以直接对照:
| 现象 | 大概率原因 | 排查手段 | 处理方式 |
|---|---|---|---|
| 首页打开后刷新404 | history模式缺少try_files | curl -I http://ip/user/xxx 返回404 | location / 加 try_files |
| 页面打开,接口404 | proxy_pass路径没剥掉或剥多了 | 看后端日志或直接curl /api/xxx | 调整proxy_pass末尾斜杠 |
| 白屏且Network资源404 | publicPath/base和部署路径不匹配 | 看index.html里script src路径 | 修改publicPath后重新打包 |
| 提示跨域 | 前端代码写死了跨域地址 | 看浏览器请求URL | 改为同源相对路径,走Nginx代理 |
| 上传文件报413 | client_max_body_size太小 | 看error.log里有413 | 加大client_max_body_size |
| 后端看到所有请求IP都是127.0.0.1 | 缺少X-Forwarded-For配置 | 后端打印remoteAddr确认 | 补proxy_set_header配置 |
| 配置改完没生效 | 没reload或语法错误 | nginx -t 后 nginx -s reload | 修正语法后reload |
4.2 排查三板斧:curl、日志、nginx -t
遇到线上问题,别慌,按顺序来。
第一板斧是 curl。页面打不开,先curl -I http://127.0.0.1看服务是否活着;接口报错,先curl http://127.0.0.1/api/user/list看返回内容。curl 能直接还原请求行为,比浏览器缓存里的结果可靠得多。
第二板斧是看日志。Nginx 的访问日志和错误日志在/var/log/nginx/下,常见的是 access.log 和 error.log。用tail -f /var/log/nginx/error.log持续观察,任何配置或路径问题都会在这里留下线索。比如 404 会显示文件路径,502 会显示上游连接失败,这些信息对定位问题至关重要。
第三板斧是nginx -t。改完配置第一时间执行,语法错误会直接告诉你。很多人图省事,改完直接 reload,结果带着错误配置上线,整个站点直接不可用。这个习惯必须养好。
另外补充一个点:Nginx 在 HTTP 层转发时,客户端的 TCP 五元组信息不会原样透传给后端,后端看到的源 IP 默认是 Nginx 所在机器的内网 IP。如果需要真实客户端 IP,必须依赖X-Forwarded-For、X-Real-IP这些头部,这也是速查表里那一条配置存在的意义。
4.3 一台服务器部署多个Vue项目的两种姿势
实际工作里,一台服务器上挂好几个前端项目是很常见的事。有两种主流方案。
第一种,不同端口。每个项目一个 server 块,listen 不同的端口:
server { listen 8081; root /var/www/app1; location / { try_files $uri $uri/ /index.html; } } server { listen 8082; root /var/www/app2; location / { try_files $uri $uri/ /index.html; } }这种方式配置简单,项目隔离干净,但每个项目都要占一个端口,访问时要带上端口号。
第二种,同端口不同子路径。所有项目共用一个 80 端口,然后用 location 区分。这种方式部署时,Vue 打包的 publicPath 必须和子路径一致,比如项目A打包时设置publicPath='/app1/',项目B设置publicPath='/app2/'。Nginx 配置有点像这样:
location /app1/ { alias /var/www/app1/; index index.html; try_files $uri $uri/ /app1/index.html; } location /app2/ { alias /var/www/app2/; index index.html; try_files $uri $uri/ /app2/index.html; }这里必须注意 root 和 alias 的区别。root 会把 location 后面的路径拼在 root 后面,alias 则直接用 alias 指定的路径替换掉 location 匹配的部分。子路径部署最怕 alias 写错,目录对不上就全是404。我自己的经验是:子路径部署尽量用 alias,路径映射关系一眼就能看清。
4.4 顺手就做的性能与安全小优化
部署上线不等于结束,以下几个配置我一般会顺手加上。
开启 gzip 压缩,减少传输体积,对前端项目收益明显:
gzip on; gzip_types text/css application/javascript application/json image/svg+xml; gzip_min_length 1k; gzip_vary on;静态资源缓存。打包后的文件通常带 hash,内容一变文件名就变,非常适合强缓存。上面模板里location /assets/的expires 7d就是这个思路。需要注意,index.html 本身不能做强缓存,否则发新版后用户加载的还是旧 HTML。
加client_max_body_size 20m;放在 http 或 server 层,避免上传功能出现 413。默认值只有 1m,做文件上传的项目不加大必踩坑。
加server_tokens off;隐藏 Nginx 版本号,减少被扫描的风险。这算是最基础的安全加固了。
还有个冷门但实用的经验:如果项目里有 m3u8 这类视频流播放需求,Nginx 默认 MIME type 可能不认这些格式,浏览器会拒绝播放。需要在 location 里加上对应的 types,比如application/vnd.apple.mpegurl m3u8; video/mp2t ts;。这类问题平时不好遇到,但真遇到时排查方向就对了。
我个人把部署经验总结为一句话:配置 Nginx 不是背指令,而是搞清楚一个 URL 请求进来,Nginx 怎么一步步找到文件和转发请求。把 location 优先级、try_files 兜底、proxy_pass 的路径替换这三件事理解透,Vue 项目部署里 80% 的问题都能解决,剩下 20% 大多能在 error.log 里找到答案。
最后再分享一个小习惯:每次改完配置文件,一定要先nginx -t再nginx -s reload,别嫌麻烦。有一次我图快直接 reload,结果配置里 root 目录写错,整个站点直接 502,那次教训到现在我都记着。部署这件事,稳比快重要得多。