ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

若依前后端分离Nginx部署:刷新404、验证码、静态资源与接口404排查

若依前后端分离Nginx部署:刷新404、验证码、静态资源与接口404排查 上周接了个活把公司内部一套若依RuoYi前后端分离的管理系统从开发机上挪到测试服务器的 Nginx 上。说实话这东西在本地npm run dev的时候乖巧得很一上 Nginx 就跟换了个人似的首页点得进去子页面也能翻可手一抖按了 F5 直接给你甩个 404登录页的验证码位置空着控制台红成一片后端接口全变成 404F12 的 Network 面板里躺着几十条红杠。就这么几个问题我从下午两点折腾到快七点才一个个填平。这篇就把若依 Vue 前端加上 Nginx 部署这条链路上最容易崩的几处讲透页面刷新 404、验证码找不到、系统静态资源 404、后端接口 404四类问题的根因、排查路径和修法全写清楚。不管你是头一回部署若依还是从别的框架转过来踩了同样的坑配置都能直接抄过去用。全程只讲实操不讲虚的。1. 先搞清楚若依前后端分离这套东西到底怎么跑起来的1.1 前端产物长什么样、接口前缀是从哪冒出来的若依前后端分离版前端仓库是 RuoYi-VueVue2 加 Vue CLI或者 RuoYi-Vue3Vue3 加 Vite。两个版本打包逻辑不一样但产出的东西差不多一个dist目录里面有index.html以及static/js、static/css、static/img这些子目录再加一些字体图标文件。真正决定部署成败的是前端项目根目录下那个.env.production。RuoYi-Vue 里大概长这样ENV production VUE_APP_BASE_API /prod-apiRuoYi-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.htmlhistory 路由刷新也不会 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。后端处理完返回 JSONNginx 再原样吐回浏览器。三条链路三种处理方式。任何一个环节没配对就是 404。想明白了这个下面的问题就都是填空题了。2. 页面刷新 404history 路由模式和 try_files 才是主角2.1 先把现象和根因对上号现象特别有迷惑性从首页点导航进/system/user一切正常但只要在/system/user这个页面上按 F5或者直接把地址复制到新标签页打开立刻 404页面显示 Nginx 的默认错误页。根因就在前端路由的 mode 上。RuoYi-Vue 的src/router/index.jsexport 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_fileslocation / { 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.jspublicPath改成/ruoyi/src/router/index.jsnew Router({ base })改成/ruoyi/Nginxlocation改成location /ruoyi/try_files回退到/ruoyi/index.htmlpublicPath管的是打包后index.html里引用的 js/css 路径。默认是/生成的是script src/static/js/app.xxx.js改成/ruoyi/之后变成/ruoyi/static/js/app.xxx.jsNginx 才能找得到。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。解决办法就是加一个反向代理的 locationlocation /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/captchaImage404第二种写法后端收到的是带着/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.pngalias则是直接替换变成/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 请求头没透传表现出的症状很像 404proxy_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 现象到原因的速查表现象最可能的原因定位方法处理首页正常刷新子页面 404history 模式缺 try_files看 URL 是否带#加try_files $uri $uri/ /index.html验证码空白captchaImage 404/prod-api没配代理Network 看响应体是不是 Nginx 页加location /prod-api/接口 404 且路径里带 prod-apiproxy_pass没带结尾斜杠curl 后端看收到什么路径加结尾/接口 404 且后端日志无记录前端还在请求本机地址看请求的域名检查VUE_APP_BASE_API样式丢失js/css 404publicPath 与 root 不一致比对 URL 与磁盘路径改publicPath或root上传图片打不开/profile/未映射看请求路径前缀加location /profile/加alias字体图标加载失败MIME 类型或跨域看响应头 Content-Type补字体后缀的 location容器里接口连不上127.0.0.1 指向容器自身容器内 curl 宿主机用服务名或宿主 IP改配置没效果忘了 reload 或语法报错看nginx -t输出nginx -s reload6.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/svgxml;如果服务器前面还挂了一层 HTTPS 终结记得把X-Forwarded-Proto透传下去并且后端配置里开启对应的代理头识别否则后端生成的重定向地址协议会错。再就是日志。若依部署完之后access.log里能看到所有请求的路径和状态码排查 404 的时候比翻浏览器 Network 更全因为它记录了包括那些没被前端代码触发的请求。我一般会先 tail 一下日志再动手改配置。最后一点如果同一台服务器上要部署多个前端项目建议每个项目一个独立的server块用server_name区分而不是在一个server里堆一堆location。后者写着写着 location 优先级就乱了排查成本陡增。多项目共用同一个域名的话就用子路径区分每个项目一个location各自配好root和try_files井水不犯河水。这套东西从下午折腾到晚上回过头看其实每一步都有清晰的原因只是当时被一个接一个的 404 打乱了节奏。真正有用的是那套排查顺序先看响应体是谁生成的把问题切到某一层再往下挖。配置本身网上到处都是能判断出该抄哪一份才是省时间的地方。
返回列表