1. 刷新就404,问题到底出在哪
先说结论,这类“前端项目部署到Nginx,首次访问一切正常,一刷新就404”的报错,十有八九是SPA路由和Nginx静态文件服务之间的机制没对齐。标题里那串“httpslocalhostindex”,翻译成人话就是:HTTPS域名下的前端应用,入口是 index.html,配好Nginx后首屏能打开,但压F5直接404。碰到这种情况,先别急着怀疑证书、怀疑防火墙,绝大多数新手栽在同一个地方:没有配置前端路由回退。
为什么首次访问没事,刷新就挂?我拆开讲。浏览器第一次访问https://example.com/about时,用户往往是从首页点导航跳转过去的。Vue Router 或 React Router 这一类前端路由库,并不会让浏览器重新发一次请求,而是通过 History API 的pushState悄悄改掉地址栏URL。前端路由在当前页面内存里把/about解析成对应的组件并渲染出来,所以页面内容确实变了,但网络请求压根没发生。
但只要你按F5刷新,浏览器可不管什么前端路由,它老老实实照着地址栏的URL,去向服务器请求/about这个路径。Nginx收到请求后会到站点root目录里找一个叫about的物理文件,找不到,默认就返回404。问题就出在这:SPA应用只有一个真正的HTML文件,也就是index.html,其他路径都是前端路由模拟出来的“假页面”。Nginx不知道/about应该交给前端路由去处理,它只会按照静态文件服务器的逻辑去磁盘找文件。所以你要做的,是告诉Nginx:当请求的路径找不到对应文件时,别急着返回404,先把请求交给index.html,让前端路由自己决定展示什么。
有人会问,我明明配置了location / { root ...; index index.html; },怎么还是404?这就是典型的“半配置”状态。index指令只解决访问根目录/时展示哪个文件,它不负责/about这类深链接。要让深链接在刷新时也能找到入口,就得靠try_files把路由回退统一交给index.html。这也是绝大多数刷新404场景的标准解法。这篇文章就是围绕这个展开,适合前端开发者、刚接触部署的运维同学,以及所有被Nginx折磨过的人参考。
2. nginx的location匹配逻辑——为什么你的配置看起来没问题却还是404
2.1 location的匹配优先级
Nginx处理请求时,第一步是匹配location块。location的匹配有一套固定优先级:精确匹配=最高,其次是不带正则的^~,再往下是正则匹配~/~*,最后才是普通前缀匹配。很多人只写一个location / { ... },以为把所有请求都拦下来了,但实际上Nginx会按优先级选最合适的location,而不是按配置文件的先后顺序。
举个实际例子。假设同时存在location = /index.html和location /,访问/index.html时命中前者而非后者。如果某个静态资源路径恰好命中了更精确或正则的location,配置逻辑就可能分叉,导致404。比如有人给图片目录单独写了正则location,里面只做了缓存头,没加try_files,结果图片刷新时反而404。这类问题排查起来特别费劲,因为你看location /里明明配置得很完整,但请求根本没进这个块。
2.2 root和alias的拼接差异
刷新404还有一种变体,页面本身能打开,但CSS、JS全部404。这种情况大多是root和alias傻傻分不清。root会拿完整URI去拼接目录,alias则是把location匹配的部分整段替换掉。我见过太多人在这一步翻车。
location /static/ { root /www/app; # 访问 /static/js/app.js → 实际找 /www/app/static/js/app.js } location /static/ { alias /www/public; # 访问 /static/js/app.js → 实际找 /www/public/js/app.js }看到区别了吗?root保留完整URI,alias相当于把URI前缀“/static/”剥掉,换成另一段路径。如果项目静态资源单独部署或者打包后资源路径带子目录,配置里搞错这一层,页面HTML能回来但资源全404,看起来就像服务器抽风。
再加一个更隐蔽的坑:alias指向目录时,末尾的斜杠经常被人忽略。alias /www/public和alias /www/public/在实际拼接时可能差出一个目录层级。我自己踩过一次,前端打包资源放在 dist/assets 下,Nginx配置里 alias 漏了斜杠,结果所有JS请求都变成了/www/publicassets/...,列表页能开,但样式全乱,F12一片红。这种404在浏览器里看请求路径非常容易误判。
2.3 try_files的机制和常见误用
try_files的逻辑是:按顺序检查文件是否存在,命中第一个存在的就返回;全部不存在,则执行最后一个参数。最常见的SPA配置长这样:
location / { root /www/app/dist; index index.html; try_files $uri $uri/ /index.html; }$uri是当前请求的路径,比如/about。Nginx先尝试找/www/app/dist/about这个文件,不存在;再尝试$uri/,也就是把/about当目录,去看/www/app/dist/about/,如果目录存在还要继续找这个目录下的index文件;都没有,就内部重定向到/index.html。注意这里发生了第二次location匹配,/index.html会重新走进location /的块,然后找到/www/app/dist/index.html返回。
很多人把最后一项写成/,以为能重定向到根路径,结果刷新后首页是出来了,但浏览器地址栏停在/about,页面内容却渲染成了首页,路由状态完全对不上,体验同样很怪。还有人把try_files写成try_files $uri $uri/ =404;,这种“硬派”写法等于直接把路由回退废掉,等于没配。另外注意,$uri/这一项也别乱删,后面我会细说它存在的意义。
3. 标准解法:一套能直接抄的Nginx配置
3.1 完整配置模板
直接给出一份能用的模板,我把注释写在旁边,抄的时候对着改域名和路径就行。
server { listen 443 ssl http2; server_name example.com; # SSL证书配置这里省略,按你实际情况填 root /www/app/dist; index index.html; # 关键:前端路由回退 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存,按需使用 location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { expires 30d; add_header Cache-Control "public, immutable"; } # 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; } }核心就是location /里的三行:root、index、try_files。很多人只写了前两行,漏了try_files,刷新必炸。模板里把API单独拆到location /api/,是因为接口请求绝对不能回退到index.html,否则后端接口全废。
3.2 为什么$uri/要放在第二位
try_files 里$uri/这一项,不少人觉得多余:SPA项目磁盘上根本没有对应目录,为什么不直接写成try_files $uri /index.html;?我的建议是保留,因为$uri/能处理那些确实存在的目录请求。
举个例子。项目dist目录下有个static子目录,里面存了上传的图片。你访问/static/logo.png时,文件存在,直接用$uri就能命中,不需要$uri/。但如果你访问的是/static/这个目录本身,$uri对应的是/static这个文件的探测,大概率找不到(因为它是目录不是文件),这时候$uri/就起作用了,Nginx会检查目录是否存在,存在则进一步索引目录下的index文件。如果只有$uri /index.html,访问/static/会直接回退到前端路由,而前端路由又没有/static/这个页面,最后显示前端404页面,反而把问题搞复杂。
3.3 接口请求不能走try_files回退
有些项目API路径不是/api,直接写在前端路由同级的location下,刷新时/api/user/info这样的请求被try_files回退到index.html,后端接口完全进不去。这个问题必须靠更精确的location把API拦下来。
Nginx的普通前缀匹配选最长匹配,/api/比/长,所以location /api/ { proxy_pass ... }优先级天然高于location /,不需要调整顺序,只要两个块都存在就能正确分流。如果你本来就没有API代理需求,那location /api/删掉也没问题;但如果你有,就一定不要把它放进location /的try_files逻辑里。
3.4 proxy_pass结尾斜杠的坑
有些项目连接口也404,而且不是刷新才404,是每次请求都404。排查时发现proxy_pass的URI部分写错了。这就是那种比前端路由404更隐蔽的坑,网络请求已经发出去了,也收到响应了,状态码确实是404,但后端日志里根本查不到对应路由。
location /api/ { proxy_pass http://127.0.0.1:8080; }上面这种写法,请求/api/login会原样转发给后端,后端收到的路径是/api/login。
location /api/ { proxy_pass http://127.0.0.1:8080/; }下面这种写法,结尾多了个斜杠,Nginx会把匹配到的/api/前缀替换成/,后端收到的路径变成了/login。如果后端路由是按/api/login写的,自然就404了。每次看到这种问题,我都建议先检查proxy_pass末尾到底有没有斜杠,这是反向代理场景出错频率最高的细节。
4. 刷新404排查清单:从现象到根因的快速定位
4.1 用curl还原问题
遇到刷新404,先别急着翻代码,用curl把问题复现出来再说。直接在服务器上或者本地执行:
curl -v https://example.com/about-v参数会输出完整的请求和响应头。重点看两处:响应状态码,以及响应体内容。如果返回的是index.html的内容但状态码是200,说明Nginx配置没问题,请求已经正确回退到了前端入口,问题多半出在前端路由配置上,比如history模式没开或者路由表缺失。如果状态码是404且响应体是Nginx默认的404页面,那问题就在Nginx侧,继续往下查配置。
这个动作能帮你快速判断责任方,或者用运维的话说,快速甩锅。是前端问题还是Nginx问题,curl一测基本就能定性,避免两边互相踢皮球。我有一次排查线上问题,前端说后端接口404,后段说前端打的路径不对,最后curl出来发现是Nginx的proxy_pass斜杠问题,三方各背一口锅。
4.2 检查日志和语法
改动配置文件之前,先看日志。Nginx的日志是排查这类问题的利器。
tail -f /var/log/nginx/access.log tail -f /var/log/nginx/error.logaccess.log能看到请求是否真的进了Nginx,请求的路径是什么,返回状态码是多少。error.log能看到文件路径拼接的实际结果,比如open() "/www/app/dist/about" failed (2: No such file or directory)这种记录。看到这句基本就能确认是try_files没配,或者root路径不对。
改完配置之后,有几步必做:
nginx -t systemctl reload nginxnginx -t检查语法,确认没问题再reload。有个细节,如果报[emerg]开头的错误,说明配置语义有问题,不只是语法错误,比如proxy_pass的地址解析不了。改完配置不reload就不生效,这句话听起来是废话,但真有人卡在这一步,改完配置发现还是404,愣了半天才发现服务没重载。
4.3 是不是把history模式和hash模式搞混了
前端路由history模式依赖服务端配合,这是刷新404的根源。开发环境跑npm run dev时一切正常,是因为开发服务器内置了history fallback,比如Vite dev server默认把所有未匹配路径都转回index.html。但生产环境换了Nginx,没人做这个处理后,刷新404就出来了。
有的项目为了省事,直接改成createWebHashHistory,URL变成https://example.com/#/about。#号后面的内容不会发到服务器,刷新时浏览器只会请求/,所以永远不会404。这个方案能立刻绕开问题,但代价是URL带个#号,不美观,而且对SEO不友好。我的建议是,项目能维护一套生产配置就尽量用createWebHistory加Nginx回退,别为了省事放弃正经方案。如果确实没精力维护Nginx,hash模式也够用,关键是自己心里有数。
4.4 别把缓存问题和路由问题混为一谈
刷新404还有一种迷惑性很强的形态:第一次访问正常,刷新后页面空白,控制台报了一堆资源加载失败。这种多半不是路由的锅,而是打包资源路径或缓存策略的问题。
举个例子。vue.config.js或Vite配置里base设成了/app/,但Nginx部署时没有在/app/路径下提供资源,HTML能加载但里面的JS/CSS全部404。或者CDN缓存了旧版本的HTML,里面引用的JS文件哈希已经失效,刷新后拿到旧HTML,资源路径全是404。这种问题有一个特征:URL路径没变,但控制台资源加载错误一片红,和单纯的刷新404有本质区别。
排查方法很简单,打开浏览器开发者工具的Network面板,看是文档请求返回404,还是资源请求返回404。文档请求404走路由回退排查流程;资源请求404走路径和缓存排查流程。方向对了,问题就解决了一半。
5. 从排查到预防:几个我踩过的坑和部署自检清单
5.1 未刷新也404?先分清楚是Nginx还是前端路由的锅
有一种情况更让人抓狂:首次访问某个地址就404,根本不用等到刷新。这时候先别急着查Nginx,用curl测一下请求返回的是什么内容。如果返回Nginx风格的404页面,说明Nginx就没找到对应文件,继续查root和try_files;如果页面返回的是200但内容是前端自己的404组件,比如Vue Router配置了一个path: '/:pathMatch(.*)*'的NotFound页面,那Nginx没问题,是前端路由表里没有这个地址对应的组件。
我帮同事排查过一次线上问题,新上线的H5页面,用户反馈部分链接打开404。我在本地用curl测,返回的是200,页面内容一看就是前端路由渲染出来的NotFound组件。最后定位到是路由表里少配了一个动态路由参数的正则规则,Nginx白白背了几天锅。这个区分非常重要,不然你会花一整天在Nginx配置里找根本不存在的bug。
5.2 部署前自检清单
被404折磨过几次之后,我养成了一个习惯,每次部署前端项目都按固定清单过一遍。分享出来给大家参考:
- 确认项目打包后确实存在
index.html,并且构建产物路径与Nginx的root指向一致 - 确认
location /里有try_files $uri $uri/ /index.html;,缺了必炸 - 确认API请求有独立的location,且
proxy_pass末尾斜杠按预期配置 - 确认页面资源请求路径与打包配置的base路径一致
- 在浏览器无痕模式或代理工具里测试刷新,排除CDN和浏览器缓存干扰
- 在服务器本机用curl测试关键URL,避免被本地前端开发服务器的假象迷惑
这套清单看着简单,但每一条都对应着真实的故障案例。尤其是第五条,本地开发环境和线上Nginx是两个完全不同的服务,很多人拿开发环境的行为去类比生产环境,思维上就会出错。我在实际项目中见过太多“本地好好的,一上线就404”的情况,都是环境行为差异导致的。
5.3 一点个人体会
刷404这个问题,本质上不是Nginx难,而是两个系统之间对“路由”这个概念的理解不一样。前端路由活在浏览器内存里,Nginx活在文件系统上,中间需要一个桥接层。try_files就是这座桥。配置明白这一点,往后不管换什么服务器,遇到类似问题都能一眼看穿。
我自己的习惯是,碰到类似问题先写一个最小的服务器配置模板放在项目仓库里,不管是Nginx还是其他静态服务器,都把前端路由回退规则沉淀下来。团队新人部署时直接复制,不用每次重新踩坑。这种经验的积累,比多会一个命令更有价值。