上周接了个活,把公司内部一套若依(RuoYi)前后端分离的管理系统,从开发机上挪到测试服务器的 Nginx 上。说实话,这东西在本地npm run dev的时候乖巧得很,一上 Nginx 就跟换了个人似的:首页点得进去,子页面也能翻,可手一抖按了 F5 直接给你甩个 404;登录页的验证码位置空着,控制台红成一片;后端接口全变成 404,F12 的 Network 面板里躺着几十条红杠。就这么几个问题,我从下午两点折腾到快七点,才一个个填平。
这篇就把若依 Vue 前端加上 Nginx 部署这条链路上最容易崩的几处讲透,页面刷新 404、验证码找不到、系统静态资源 404、后端接口 404,四类问题的根因、排查路径和修法全写清楚。不管你是头一回部署若依,还是从别的框架转过来踩了同样的坑,配置都能直接抄过去用。全程只讲实操,不讲虚的。
1. 先搞清楚若依前后端分离这套东西到底怎么跑起来的
1.1 前端产物长什么样、接口前缀是从哪冒出来的
若依前后端分离版,前端仓库是 RuoYi-Vue(Vue2 加 Vue CLI)或者 RuoYi-Vue3(Vue3 加 Vite)。两个版本打包逻辑不一样,但产出的东西差不多:一个dist目录,里面有index.html,以及static/js、static/css、static/img这些子目录,再加一些字体图标文件。
真正决定部署成败的,是前端项目根目录下那个.env.production。RuoYi-Vue 里大概长这样:
ENV = 'production' VUE_APP_BASE_API = '/prod-api'RuoYi-Vue3 换成了 Vite 的写法:
VITE_APP_BASE_API = '/prod-api'这一行'/prod-api'是整个部署链路里最重要的一个变量。前端所有 axios 请求的 baseURL 都取自它,也就是说,页面上任何一次接口调用,实际发出去的地址都是/prod-api/xxxx。登录页拿验证码,请求的是/prod-api/captchaImage;点登录,请求的是/prod-api/login。
而后端 Spring Boot 那边,接口本身是不带这个前缀的。验证码就是/captchaImage,登录就是/login,用户列表就是/system/user/list。
于是就出现了一个必须先想明白的问题:前端发出去带/prod-api,后端认的是不带前缀的路径,中间这个差值,得有人补上。
1.2 为什么本地开发一切正常,一上 Nginx 就满地 404
补差值的那个人,在开发环境里是 dev-server。
RuoYi-Vue 的vue.config.js里有这么一段:
devServer: { port: 80, proxy: { [process.env.VUE_APP_BASE_API]: { target: `http://localhost:8080`, changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' } } } }这段配置干了两件事:一是把所有/prod-api开头的请求转发到http://localhost:8080,二是用pathRewrite把/prod-api这个前缀抹掉。而 dev-server 本身又是个 SPA 服务器,任何找不到的路径都会兜底返回index.html,history 路由刷新也不会 404。
换句话说,开发环境里"转发"和"兜底"这两件事,是 dev-server 免费送你的。
一旦换成 Nginx,这份免费的午餐就没了。Nginx 默认只干一件事:按路径去磁盘上找文件。找不到就 404,它不会帮你转发,也不会帮你兜底。所以你得手动把 dev-server 做过的两件事,在 Nginx 配置里一条一条补回来。搞不清楚这一点,后面所有的 404 都会看起来莫名其妙。
1.3 完整的请求链路应该长什么样
在动手配之前,脑子里先有个清晰的数据流,后面排查能省一半时间。
浏览器访问http://your-server/,Nginx 从dist目录返回index.html。页面加载完,前端路由接管。这一步是纯静态的,跟后端没关系。
用户在地址栏敲http://your-server/system/user并回车,浏览器真的发了一次请求。Nginx 在dist目录下找不到system/user这个文件,这时候就需要try_files把它兜回index.html,再由 Vue Router 解析出该渲染哪个页面。
页面渲染过程中调接口,请求http://your-server/prod-api/system/user/list。Nginx 一看路径以/prod-api/开头,走反向代理,把请求转给http://127.0.0.1:8080/system/user/list。后端处理完返回 JSON,Nginx 再原样吐回浏览器。
三条链路,三种处理方式。任何一个环节没配对,就是 404。想明白了这个,下面的问题就都是填空题了。
2. 页面刷新 404:history 路由模式和 try_files 才是主角
2.1 先把现象和根因对上号
现象特别有迷惑性:从首页点导航进/system/user,一切正常;但只要在/system/user这个页面上按 F5,或者直接把地址复制到新标签页打开,立刻 404,页面显示 Nginx 的默认错误页。
根因就在前端路由的 mode 上。RuoYi-Vue 的src/router/index.js:
export default new Router({ mode: 'history', scrollBehavior: () => ({ y: 0 }), routes: constantRoutes })history模式意味着 URL 里没有#。前端点导航时,Vue Router 用history.pushState改了地址栏,但并没有真的发请求,页面由前端自己渲染,所以看起来一切正常。
可一旦刷新,浏览器就会拿当前这个"完整路径"去服务器要资源。服务器上根本没有system/user这个文件,也没有这个目录,Nginx 只能回 404。
反过来说,如果 mode 是hash,地址栏是http://your-server/#/system/user,#后面的内容浏览器不会发给服务器,服务器收到的永远是/,自然不会有 404。
2.2 try_files 一行配置解决,但写法有讲究
解决方案就是在location /里加一句try_files:
location / { root /home/ruoyi/dist; index index.html; try_files $uri $uri/ /index.html; }try_files的执行顺序是:先拿$uri去 root 目录下找同名文件,找到了就直接返回;找不到就试$uri/,也就是当成目录去找;两个都失败,内部重定向到最后一个参数/index.html。所谓"内部重定向",是指 Nginx 在服务端自己换了个处理路径,浏览器地址栏不变,用户感知不到。index.html加载后,Vue Router 读当前 URL,渲染对应组件。
几个容易翻车的细节:
root指向的必须是index.html真正所在的那个目录。很多人把dist目录整个拷贝到/usr/share/nginx/html/dist下,但root还写着/usr/share/nginx/html,结果 Nginx 去找/usr/share/nginx/html/index.html,找不到,报 500。要么把dist里的文件拷到 root 目录,要么把 root 直接指到dist。
try_files最后那个回退路径,写/index.html而不是index.html,更稳妥。前者始终从 root 开始匹配。
$uri/这一项我一般会保留,但在某些目录结构下它会捣乱。比如 root 目录下恰好存在一个和路由同名的真实目录,Nginx 会去那个目录里找 index 文件,找不到就返回 403 或者目录列表,反而把真正的问题藏起来。如果你怎么配都不对,可以试试把它去掉,写成try_files $uri /index.html;。
提示:改完配置一定要
nginx -t检查语法,再nginx -s reload。我见过太多次改完忘了 reload,然后对着屏幕怀疑人生的。
2.3 部署在子目录时,三处配置必须一起改
有个场景很常见:服务器上不止一套系统,若依只能挂在/ruoyi/这样的子路径下。这时候有三处必须同步修改,少改一处就是满屏 404。
| 位置 | 配置项 | 改法 |
|---|---|---|
vue.config.js | publicPath | 改成/ruoyi/ |
src/router/index.js | new Router({ base }) | 改成/ruoyi/ |
| Nginx | location | 改成location /ruoyi/,try_files回退到/ruoyi/index.html |
publicPath管的是打包后index.html里引用的 js/css 路径。默认是/,生成的是<script src="/static/js/app.xxx.js">;改成/ruoyi/之后变成/ruoyi/static/js/app.xxx.js,Nginx 才能找得到。Router 的base管的是前端路由的根路径,不配的话路由跳转和实际地址对不上。
这三处是联动的,我当时只改了 Nginx,前端没重新打包,结果首页能打开但样式全丢,控制台一堆静态资源 404,排查了半天才反应过来。
3. 验证码找不到、静态资源 404:全栽在 /prod-api 这个前缀上
3.1 验证码接口到底请求到哪去了
登录页打开,验证码那块是一片空白,点一下输入框旁边的刷新按钮也没反应。按 F12 打开 Network,一眼就看到/prod-api/captchaImage这个请求,状态码 404,响应体是 Nginx 的 HTML 错误页。
这个响应体形态很关键。如果 404 页面的内容是 Nginx 自己生成的,说明请求根本没到后端,问题在 Nginx 这一层;如果响应体是后端返回的 JSON,说明请求到了后端,只是后端没有对应路径。这两个判断能帮你瞬间把问题切成两半。
这里显然是前者。原因是 Nginx 里只配了location /,所有请求,包括/prod-api/captchaImage,都被当成静态文件去dist目录里找了。dist目录下当然没有叫prod-api的东西,404。
解决办法就是加一个反向代理的 location:
location /prod-api/ { proxy_pass http://127.0.0.1:8080/; }加完 reload,验证码立刻出来了。
3.2 proxy_pass 结尾那个斜杠能坑你一整天
这是若依部署里最经典、也最容易反复踩的一个坑。上面那行配置,proxy_pass后面的地址写了http://127.0.0.1:8080/,注意结尾那个斜杠。有没有它,后端收到的路径完全不一样。
Nginx 的规则是这样的:proxy_pass后面如果带了 URI 部分(哪怕只是单个/),Nginx 会把location匹配到的那段前缀替换成这个 URI;如果不带 URI,也就是只有http://127.0.0.1:8080,就把原始路径原样透传过去。
拿location /prod-api/举例,对比一下就清楚了:
proxy_pass写法 | 浏览器请求 | 后端实际收到 | 结果 |
|---|---|---|---|
http://127.0.0.1:8080/ | /prod-api/captchaImage | /captchaImage | 正常 |
http://127.0.0.1:8080 | /prod-api/captchaImage | /prod-api/captchaImage | 404 |
第二种写法,后端收到的是带着/prod-api的路径,而 Spring Boot 里根本没注册这个路径的接口,自然 404。这也是很多人遇到的"代理明明通了,端口也通,但接口就是 404"的根本原因——判断"通没通"不能只看能不能连上后端,要看后端收到的路径对不对。
还有个细节:location /prod-api/结尾也带斜杠,这是为了精确匹配到以/prod-api/开头的所有请求。如果写成location /prod-api,它同样能匹配/prod-api/xxx,但也会匹配/prod-api-other这种无关路径,属于给自己埋雷。
3.3 前端静态资源 404 的三种典型情况
除了验证码,第二种高频 404 出现在静态资源上。表现是页面能打开,但样式全丢、图标不显示、JS 报错。这三类情况要分开看。
第一类是 JS、CSS、图片这些打包产物加载不到。原因几乎都是publicPath和 Nginxroot对不上——打包时按/ruoyi/生成路径,Nginx 却按根路径去找,或者dist目录摆放位置和root不一致。判断方法很简单:看 Network 里那个 404 请求的完整 URL,把路径和服务器上的实际文件位置对一遍,问题一目了然。
第二类是字体图标加载失败,比如 element-ui 的element-icons.woff。这类翻车通常是 MIME 类型不对或者跨域。Nginx 有时候不认识woff2、ttf这些后缀,返回Content-Type: application/octet-stream,浏览器直接拒绝加载。补一段:
location ~* \.(woff2?|ttf|eot|svg)$ { root /home/ruoyi/dist; add_header Access-Control-Allow-Origin *; expires 30d; }第三类是后端上传的图片、附件 404。若依后端有个资源映射,把/profile/**映射到本地上传目录。走整站反向代理的时候,这类请求会被一起转给后端,后端自己会处理。但如果你的 Nginx 只代理了/prod-api/,那/profile/xxx.png就会落到location /里去 dist 目录找,必然 404。解决办法是让 Nginx 直接托管上传目录:
location /profile/ { alias /home/ruoyi/uploadPath/; }这里必须用alias而不是root。两者的区别是:root会把location的路径拼到后面,变成/home/ruoyi/uploadPath/profile/xxx.png;alias则是直接替换,变成/home/ruoyi/uploadPath/xxx.png。对于/profile/这种映射目录,通常要的是后者。另外alias和location的结尾斜杠最好保持一致,两边都带或者都不带,否则路径会多一层或者少一层。
4. 后端接口 404:代理通了但路径不对的几种死法
4.1 先用 curl 把问题一刀切开
遇到接口 404,别急着改配置,先在服务器上敲两条命令:
curl -i http://127.0.0.1/prod-api/captchaImage curl -i http://127.0.0.1:8080/captchaImage第一条走 Nginx,第二条直连后端。对比结果的三种情况:
两条都 404,说明后端本身就没这个接口,或者服务没起来、端口不对、context-path 配错,问题不在 Nginx。
第一条 404 第二条正常,说明后端没问题,是 Nginx 的代理规则有问题,重点看proxy_pass的斜杠和location匹配。
第一条正常第二条 404,这种组合比较少见,一般是后端做了路径重写或者网关转发,得看后端配置。
这个方法比在浏览器里翻 Network 快得多,也更能排除干扰。毕竟浏览器里还有缓存、跨域、Service Worker 这些乱七八糟的东西掺和。
4.2 context-path 和 Nginx 前缀的错配
若依后端application.yml里有这么一段:
server: port: 8080 servlet: context-path: /默认是/,也就是没有前缀。这种情况下,Nginx 的proxy_pass要带结尾斜杠,把/prod-api剥掉。
但有些团队会把context-path改成/prod-api,想让后端接口本身就带这个前缀。这时候 Nginx 那边反而不能带结尾斜杠,得原样透传:
location /prod-api/ { proxy_pass http://127.0.0.1:8080; }所以配之前务必先确认后端的context-path是什么。这个信息在后端配置文件里,或者直接看后端启动日志里打印的路径。别凭感觉猜,猜错了就是来回改半天。
再延伸一点,若依的微服务版(RuoYi-Cloud)走的是网关那一套,前端请求要先到网关,再由网关路由到具体服务。这种架构下,前端VUE_APP_BASE_API指向的是网关地址,Nginx 只需要把/prod-api/代理到网关端口就行,具体哪个服务处理由网关决定。有人把整套微服务跑在单节点 K8s 上再迁移到云主机,思路其实是一样的:前端永远只认一个统一入口,路径前缀在 Nginx 或网关这一层消化掉,别让前端去关心后端有几个服务。
4.3 请求头没透传,表现出的症状很像 404
proxy_set_header这几行,很多人觉得是可选项,直接省掉。省掉的后果是后端拿到的 Host、真实 IP、协议全部丢失,进而出现一些很迷惑的现象:登录成功但跳转回内网 IP 地址、返回的资源链接是http://127.0.0.1:8080/xxx、某些鉴权逻辑判断失败。
标准写法是这四行:
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;Host用$host而不是$proxy_host,这样后端生成的重定向地址才会是用户能访问的域名,而不是内网地址。X-Forwarded-Proto在 Nginx 前面还挂了一层 HTTPS 终结的情况下尤其重要,后端要靠它判断请求是不是加密协议,否则跳转链接的协议会错。
4.4 跨域造成的假 404
还有一种情况,前端打包后接口全 404,但后端日志里一条请求记录都没有。这时候去看 Network 面板,会发现请求的地址根本不是当前域名,而是http://localhost:8080/xxx。
原因是.env.production里的VUE_APP_BASE_API没改,或者打包时用的不是 production 环境变量。前端还在直连本机后端,浏览器发出去的是跨域请求,被拦下来后报 Network Error,看起来很像 404。
生产环境的正确做法是走同源代理:前端只请求自己域名下的/prod-api/...,由 Nginx 转发到后端。这样浏览器看来前后端同源,压根没有跨域问题,后端也不用配一堆 CORS 规则。
顺便提一句,如果你用的是若依 Vue3 版本,环境变量前缀从VUE_APP_变成了VITE_,而且只有VITE_开头的变量才会被注入前端代码。有人从 Vue2 版本迁移时忘了改这个前缀,结果变量读出来是undefined,请求地址拼成了undefined/xxx,同样是一堆 404。这也是很多人从 Vue2 转 Vue3 时踩到的第一脚。
5. 一份可直接抄的 Nginx 配置与完整部署流程
5.1 打包之前要检查的几处
打包命令,Vue2 版和 Vue3 版脚本名一般相同:
npm run build:prod具体看package.json里scripts配的是什么,有些项目叫build,有些叫build:prod。
打包之前花两分钟检查三处,能省掉后面一堆返工。
第一处,.env.production里的接口前缀是不是/prod-api。如果不是,要么改这个文件,要么改 Nginx 的location,两边必须对齐。
第二处,vue.config.js里的publicPath。部署在根路径就保持/,部署在子目录就改成对应子路径。
第三处,路由的mode。确认是history就老老实实配try_files,是hash就不用配。
打包完成后dist目录里有index.html、static以及favicon.ico,把这些拷到服务器上,比如/home/ruoyi/dist。
5.2 完整 server 段配置
下面这份是我目前在用的,可以直接拿去改改就用:
server { listen 80; server_name your.domain.com; charset utf-8; client_max_body_size 50m; # 前端静态资源入口 location / { root /home/ruoyi/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /prod-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; proxy_send_timeout 60s; } # 上传文件目录 location /profile/ { alias /home/ruoyi/uploadPath/; expires 30d; } # 带 hash 的静态资源长缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2?|ttf|eot|svg)$ { root /home/ruoyi/dist; expires 7d; access_log off; } # index.html 绝不缓存,保证发版后用户拿到最新版 location = /index.html { root /home/ruoyi/dist; add_header Cache-Control "no-cache, no-store, must-revalidate"; } }client_max_body_size那行是给上传文件留的,若依默认上传限制在后端配置里,Nginx 这边如果不放开,超过 1M 的文件直接 413,很多人第一次传附件就撞上这个。
proxy_read_timeout也值得调,若依有些导出接口耗时长,默认 60 秒有时候不够,遇到超时再往上加。
5.3 一个容易忽略的优先级问题
Nginx 的 location 匹配是有优先级的,顺序大致是:精确匹配=、前缀匹配里最长的、带^~的前缀匹配、正则匹配~/~*、最后才是普通前缀匹配。上面那份配置里有个正则 location 匹配静态资源后缀,它的优先级高于普通前缀 location。
这意味着,如果某个接口的路径恰好以.js结尾(极少见,但可能存在),它会被那个正则规则截走,去 dist 目录里找文件,然后 404。正常情况下不会碰到,但知道有这么回事,排查的时候能少走弯路。
如果担心这个问题,把静态资源那个正则改成^~ /static/这样的前缀匹配会更安全:
location ^~ /static/ { root /home/ruoyi/dist; expires 7d; access_log off; }因为打包后的静态资源都在static目录下,用^~前缀匹配足够,还避开了正则优先级的坑。
5.4 部署验证的标准动作
配置改完,按这个顺序走一遍:
nginx -t nginx -s reload curl -I http://127.0.0.1/ curl -I http://127.0.0.1/static/js/app.js curl -i http://127.0.0.1/prod-api/captchaImage第一条检查语法,有错会直接告诉你哪一行。第二条重新加载配置。后面三条分别验证首页、静态资源、后端代理。
nginx -t那一步千万别跳过。配置里少个分号、多写个大括号,reload 会直接失败,而且失败的时候 Nginx 会保留旧配置继续跑,你改的那些压根没生效,很容易误判成改了没用。
5.5 用容器部署时的额外注意
如果 Nginx 跑在 Docker 容器里,proxy_pass http://127.0.0.1:8080/这个写法会出问题。容器里的127.0.0.1指的是容器自己,不是宿主机,而后端通常跑在宿主机或者另一个容器里。
解决办法有两个:要么走容器自定义网络,用服务名代替 IP,比如proxy_pass http://ruoyi-backend:8080/;;要么直接写宿主机 IP,或者用host.docker.internal配合--add-host参数。
这个坑的迷惑性在于,Nginx 本身一切正常,nginx -t也不报错,就是接口一直 502 或者 404,非常难往网络层面想。
6. 一个下午的排查套路与踩坑速查表
6.1 我自己的固定排查顺序
被这几个问题轮番教育之后,我总结出一个基本固定的排查顺序,后面再遇到类似情况,基本十分钟内能定位到是哪一层的问题。
第一步,打开 F12 的 Network 面板,找到报错的那个请求,重点看三样东西:完整的请求 URL、状态码、响应体内容。这三样能覆盖八成的判断。
第二步,看响应体是谁生成的。如果是 Nginx 的 HTML 错误页,问题在 Nginx 层,大概率是 location 没匹配上或者路径找不着。如果是后端返回的 JSON,问题在后端层,是路径对不上或者接口不存在。这一步做完,问题范围就砍掉一半。
第三步,在服务器上 curl 一次后端端口,确认后端本身是好的。如果 curl 也不通,先解决后端,别在 Nginx 上浪费时间。
第四步,如果前后端分别都正常,那就只剩中间那层,盯着location和proxy_pass看,尤其是结尾斜杠。
6.2 现象到原因的速查表
| 现象 | 最可能的原因 | 定位方法 | 处理 |
|---|---|---|---|
| 首页正常,刷新子页面 404 | history 模式缺 try_files | 看 URL 是否带# | 加try_files $uri $uri/ /index.html |
| 验证码空白,captchaImage 404 | /prod-api没配代理 | Network 看响应体是不是 Nginx 页 | 加location /prod-api/ |
| 接口 404 且路径里带 prod-api | proxy_pass没带结尾斜杠 | curl 后端看收到什么路径 | 加结尾/ |
| 接口 404 且后端日志无记录 | 前端还在请求本机地址 | 看请求的域名 | 检查VUE_APP_BASE_API |
| 样式丢失,js/css 404 | publicPath 与 root 不一致 | 比对 URL 与磁盘路径 | 改publicPath或root |
| 上传图片打不开 | /profile/未映射 | 看请求路径前缀 | 加location /profile/加alias |
| 字体图标加载失败 | MIME 类型或跨域 | 看响应头 Content-Type | 补字体后缀的 location |
| 容器里接口连不上 | 127.0.0.1 指向容器自身 | 容器内 curl 宿主机 | 用服务名或宿主 IP |
| 改配置没效果 | 忘了 reload 或语法报错 | 看nginx -t输出 | nginx -s reload |
6.3 几个让我印象深刻的坑
第一个是忘了 reload。当时改完配置刷新页面还是 404,反复检查配置内容,来回折腾了二十多分钟,最后才想起来压根没执行nginx -s reload。这个低级错误值得单列出来提醒。
第二个是location /prod-api没写结尾斜杠。这个我上面提过,它会匹配到/prod-api-anything这类无关路径,而且在某些 Nginx 版本里和proxy_pass的配合行为和带斜杠时不一样,容易产生难以理解的结果。养成习惯,写前缀 location 就带上结尾斜杠。
第三个是try_files的回退路径写错。我一开始写的是try_files $uri $uri/ index.html;,少了个斜杠。大部分时候能工作,但在某些嵌套 location 的场景下会出乱子。老老实实写/index.html最保险。
第四个是 SPA 的缓存策略。上线之后要有这个意识:index.html绝对不能长缓存,因为它是整个应用的入口壳子,里面引用的 js 文件名是带 hash 的。如果用户浏览器缓存了旧的index.html,发版之后他拿到的还是旧壳子,引用的还是旧的 js 文件,新功能一个都看不到,用户清缓存才能解决。所以一定要给index.html单独设no-cache,带 hash 的静态资源则放心地设长缓存。
6.4 后续还能顺手优化的几点
配置跑通之后,有几个小优化可以加上,成本很低但收益明显。
开 gzip 压缩,前端打包出来的 js 动辄几百 K,压缩后能砍掉三分之二,首屏加载快不少:
gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/plain text/css application/javascript application/json image/svg+xml;如果服务器前面还挂了一层 HTTPS 终结,记得把X-Forwarded-Proto透传下去,并且后端配置里开启对应的代理头识别,否则后端生成的重定向地址协议会错。
再就是日志。若依部署完之后,access.log里能看到所有请求的路径和状态码,排查 404 的时候比翻浏览器 Network 更全,因为它记录了包括那些没被前端代码触发的请求。我一般会先 tail 一下日志再动手改配置。
最后一点,如果同一台服务器上要部署多个前端项目,建议每个项目一个独立的server块,用server_name区分,而不是在一个server里堆一堆location。后者写着写着 location 优先级就乱了,排查成本陡增。多项目共用同一个域名的话,就用子路径区分,每个项目一个location,各自配好root和try_files,井水不犯河水。
这套东西从下午折腾到晚上,回过头看其实每一步都有清晰的原因,只是当时被一个接一个的 404 打乱了节奏。真正有用的是那套排查顺序:先看响应体是谁生成的,把问题切到某一层,再往下挖。配置本身网上到处都是,能判断出该抄哪一份,才是省时间的地方。