我先说个场景线上项目刚部署完首页打开一切正常前端路由跳转也都丝滑。然后你随手在地址栏直接输了一个二级路由的URL回车浏览器给你甩了个大大的白色 404。再刷新一次当前页又是404。群里马上炸开锅是不是路由配置有问题是不是构建丢文件了。排查到最后问题往往不在 Vue 代码里而在 Nginx 配置上。这个Nginx 配置 Vue 路由 404的经典场景几乎每个做前端部署的人都会踩一次我把它完整拆开讲一遍包含原理、配置、踩坑和子路径部署的进阶内容看完你不仅能修好 404还能搞懂它为什么会出现。1. 一个让我当场愣住的场景首页正常刷新却冒出 4041.1 从一次看似荒谬的报障说起事情是这样的某项目用 Vue 3 Vue Router 开发路由用的 history 模式线上域名假设叫www.example.com。部署完成之后测试人员打开www.example.com一切正常点击页面里的链接跳转到www.example.com/user/123也一切正常。然后他做了一件事在浏览器地址栏直接按回车重新请求www.example.com/user/123页面瞬间变成 404。这还不是最奇怪的最奇怪的是在user/123这个页面上随便点几个内部链接路由切换又恢复了正常。也就是说问题只在浏览器向服务器发起全新请求的时候出现在应用内部用前端路由跳转的时候永远正常。很多前端同事第一次遇到这种情况本能反应是去查 Vue Router 的代码甚至怀疑是路由懒加载的 chunk 丢失了。实际上代码没有任何问题问题发生在服务器层面user/123在服务器里根本没有对应的物理文件Nginx 找不到就直接返回了 404。1.2 先做个标准排查构建产物到底完不完整我不建议跳过排查直接改配置先确认基础项。第一步把dist目录拉下来检查index.html是否存在静态资源 JS/CSS 是否都在。第二步看一眼构建产物里的 JS 引用路径是相对路径还是绝对路径这直接影响后面子路径部署的走向。第三步用 curl 直接请求服务器看看返回的 HTTP 状态码和响应体。curl -I https://www.example.com/user/123当你看到返回404 Not Found而curl -I https://www.example.com/返回200 OK时基本可以确定 Nginx 在这个路由上没有做 SPA 回退处理。这时候你要做的不是改 Vue 代码而是去调整 Nginx 的配置让服务端把这类路由请求统一回退到index.html。2. 为什么服务器的按文件找和 Vue 的按路径画组件会打架2.1 先理解 Nginx 处理请求的底层逻辑Nginx 本质上是一个静态文件服务器加反向代理。当一个请求到达时它会根据location规则做匹配然后把请求的 URI 映射到服务器磁盘上的一个文件路径。比如你配置了location / { root /usr/share/nginx/html; }访问/user/123时Nginx 会去/usr/share/nginx/html/user/123找这个文件。磁盘上根本没有叫123的文件也没有user目录于是 Nginx 返回 404。关键点在于Nginx 的思维模型是文件系统它判断一个路径是否存在靠的是磁盘上有没有对应文件而不是看前端路由表里有没有这条路径。这是架构层面天然存在的认知差异。2.2 再理解 Vue Router history 模式的操作方式Vue Router 的 history 模式底层用的是 HTML5 History API核心是history.pushState()和history.replaceState()。这套 API 的神奇之处在于pushState会改变地址栏的 URL但不会让浏览器向服务器发送网络请求页面也不会刷新。所以当你在 SPA 内部点击router-link组件切换是纯前端的操作浏览器地址栏变了服务器完全不知道。但当你直接访问一个 URL、刷新页面、用前进后退按钮回到某个深链浏览器会向服务器发起一个真实的 HTTP 请求。这个请求到了 NginxNginx 照着磁盘找文件找不到404。2.3 冲突的核心浏览器不同情况下的请求行为差异把两个逻辑放一起看就清楚了操作浏览器行为服务器收到的请求结果打开首页请求/有index.html正常点击路由链接前端pushState不发请求无请求前端正常渲染直接访问/user/123真实请求磁盘无此文件404在/user/123刷新真实请求磁盘无此文件404前进/后退到深链真实请求磁盘无此文件404看到规律了吧。凡是触发真实 HTTP 请求的深链都会被 Nginx 按文件查找逻辑处理。Vue 觉得/user/123是自己的路由Nginx 觉得/user/123应该是一个文件两边对着同一个路径产生了完全不同的理解。2.4 顺带说说 hash 模式为什么没这个毛病有人会问那用 hash 模式不就没这些事了确实是因为 hash 模式的路由长这样www.example.com/#/user/123。浏览器地址栏#之后的片段称为 fragmentfragment 永远不会被发送到服务器。你刷新页面时请求的 URL 依然是/Nginx 正常返回index.htmlVue 读取#/user/123渲染对应组件。问题被藏在了 fragment 里。但 history 模式的好处是 URL 干净、美观也更利于 SEO 和分享。为了这个干净你就必须在服务器上补上回退逻辑。我见过不少项目为了省事直接切 hash 模式的但对一个长期维护的产品来说我更推荐把 Nginx 配好用它换回更标准的资源路径。3. 根治方案try_files 回退机制一句话讲透3.1 最小可用配置与完整配置对照解决 404 的核心配置就是一行的差别server { listen 80; server_name www.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }try_files是这段配置的灵魂。它的执行逻辑是先按顺序检查后面的参数是否存在如果存在就返回该文件如果全部不存在则交给最后一个参数做内部重定向。具体到这一行$uri表示当前请求的完整 URI 对应的磁盘文件$uri/表示对应的目录两个都不存在时内部重定向到/index.html。内部重定向之后Nginx 会重新在location /中匹配到/index.html然后把index.html作为响应返回给浏览器。浏览器拿到的是标准的 SPA 入口文件里面的 JS 启动后Vue Router 读取当前 URL 的location.pathname匹配路由表并渲染出正确的组件。整个链条至此闭合。3.2 配好后发生了什么请求流转全过程我用实际操作里的一个请求举个例子。假设用户直接访问www.example.com/order/2024/123浏览器发起 GET 请求到 NginxURI 是/order/2024/123。Nginx 匹配到location /执行try_files $uri $uri/ /index.html。Nginx 查磁盘root/order/2024/123不存在查root/order/2024/123/目录也不存在。Nginx 判定回退内部重定向到/index.html。服务器读取root/index.html返回 200Content-Type是text/html。浏览器解析 HTML加载 JS 文件Vue 应用启动。Vue Router 读取地址栏路径/order/2024/123在路由表里找到对应组件并渲染。整个过程你从浏览器 Network 面板看到的只有一个 200 的 HTML 响应和一些 JS 请求没有任何 404。用户看到的结果是地址栏那个很深的链接直接打开居然成功了刷新也不会再报错。3.3 修改完记得 nginx -t 和 reload配置文件改完别急着收工有两验证步骤一定不能省nginx -t nginx -s reloadnginx -t会检查配置语法是否正确如果有拼写错误、分号遗漏、括号不匹配它会当场报错并在终端打印具体行号。确认syntax is ok之后再 reload让新配置平滑生效。reload 不会中断当前正在处理请求的连接项目在跑的时候这样做是安全的。然后我会做一轮完整的验证不只是打开首页而是手动走这些场景直接访问一个二级路由 URL期望 200。在该路由页面按 F5 刷新期望 200。点击浏览器后退按钮期望页面能正常渲染。检查 Network 面板确认 HTML 文档请求返回的是index.html的内容。4. 无脑回退是下策三类请求必须区别对待4.1 静态资源 404 不能塞回 index.htmltry_files $uri $uri/ /index.html虽然能解决路由 404但如果整个 server 块只有一个大而全的location /那所有找不到的文件都会回退到index.html。比较典型的一个坑页面里引用的某张图片因为构建问题丢失了请求/images/banner.jpg时本该返回 404结果因为回退机制服务器返回了index.html的内容状态码是 200。浏览器在 HTML 标签里加载图片拿到一坨 HTML 且状态码 200会直接触发图片解析失败并显示裂图同时控制台报错。最难受的是因为 HTTP 状态码是 200监控系统也不会报警你得靠人肉排查才能发现。所以我的建议是如果项目的静态资源是通过版本化文件名部署的并且没有特殊需求可以考虑把回退范围限制在非静态资源上。常见写法是增加一个专门处理静态资源的 locationlocation ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 7d; try_files $uri 404; access_log off; }这里的try_files $uri 404表示找到资源就返回找不到就返回 404不要回退到index.html。正则 location 的优先级高于普通前缀 location所以这类请求会先进到这个块里处理。这是确保路由回退和静态资源 404两条逻辑不互相污染的关键一步。4.2 API 请求该走的还是走代理如果你的 Nginx 同时承担反向代理职责前端请求/api/*要转发到后端服务那这些请求绝对不能回退到index.html。一旦回退前端会收到一个 HTML 页面而不是 JSON 数据接口层全部崩掉。实际项目中我会在location /前面单独配置 API 代理location /api/ { proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }Nginx 的 location 匹配规则是有多个 location 时会优先选择前缀匹配最长的那条。/api/比/更长更精确所以所有/api/开头请求都会走代理不会进入try_files的回退逻辑。后端如果返回 404前端 fetch 能拿到真实的状态码该报错的地方能正常报错不会像无脑回退那样用 HTML 把错误包起来。4.3 匹配规则优先级location 的选择逻辑为了不踩顺序的坑掌握 Nginx location 的匹配优先级很有必要。规则大致是这样的先精确匹配再是^~的前缀匹配然后是正则匹配~/~*最后是普通前缀匹配。同一个类型里前缀匹配以最长为准正则匹配按配置文件中的顺序为准。location /favicon.ico { log_not_found off; access_log off; } location ^~ /assets/ { alias /data/web/assets/; } location /api/ { proxy_pass ...; } location / { try_files $uri $uri/ /index.html; }这个布局是我在多个项目里反复打磨过的特殊文件先拦走静态资源走 aliasAPI 走代理剩下的前端路由全部交给try_files回退。明确的分层让你后续排查问题的时候思路不混乱不至于为了一个小 404 把整份配置翻来覆去读半天。5. 部署在子路径时三个地方必须同时对齐5.1 先看真实案例/admin/ 子路径部署上面讲的都是域名根路径部署比较简单。但实际项目里经常是同一个域名下部署多个系统比如www.example.com/admin/是管理后台www.example.com/portal/是门户站点。这时候如果直接把try_files照抄上去很容易发现页面能打开但样式全丢、路由跳转到根路径、刷新深链又 404整个状态非常割裂。我接手过一个这种配置的项目当时的 Nginx 配置长这样location /admin/ { alias /usr/share/nginx/admin/; try_files $uri $uri/ /index.html; }你细看就会发现最后那个/index.html是一个根路径绝对路径Nginx 内部重定向时会跳转到根路径的index.html而不是/admin/index.html。所以用户访问/admin/user时拿到的可能是根路径应用的 HTML路由根本匹配不上。这是子路径部署最常见的失败原因Vue 路由、构建路径、Nginx 回退路径三者没有对齐。5.2 vue.config.js、Vue Router base、Nginx root 的三角关系子路径部署要打通需要三个地方同时配置第一个地方构建工具里设置公共资源路径。Vue CLI 版本在vue.config.js里写publicPath: /admin/Vite 对应的是base: /admin/。这会影响构建产物中 JS、CSS、图片等静态资源的引用前缀。第二个地方Vue Router 的 base。创建路由器时const router createRouter({ history: createWebHistory(/admin/), routes, });这样 Vue Router 在初始化时就知道自己处在/admin/子路径下内部路由的跳转都会自动带上/admin/前缀而不是跳去根路径。第三个地方Nginx 的 root/alias 和 try_files。这是最容易出错的环节。用alias的情况下location /admin/对应到的物理路径是alias指向的目录但try_files的重定向目标必须写成相对位置或者显式带上子路径。稳妥的做法是location /admin/ { alias /usr/share/nginx/admin/; try_files $uri $uri/ /admin/index.html; }特别注意最后一项写成/admin/index.html这样才能把请求引回/admin下的 SPA 入口而不是根路径的入口。5.3 try_files 在子路径下的具体写法如果项目用的是root而不是alias配置又不一样。location /admin/ { root /usr/share/nginx; try_files $uri $uri/ /admin/index.html; }这时的物理路径是/usr/share/nginx/admin/因为root会把完整 URI 拼在 root 后面。alias则是把 location 前缀替换成 alias 路径行为上有细微差别我建议优先用root 完整路径的方式逻辑更好理解也不容易写出资源路径对了但回退路径错的配置。还有个容易疏忽的点如果前端路由嵌套层级很深比如/admin/user/detail/1try_files依然能正常工作。它只看第一个参数$uri是否存在不存在就回退到/admin/index.html并不要求路由的层级和目录结构一一对应。所以只要回退路径写对了深层路由天然可用。6. 上线之后的更隐蔽问题缓存、白屏与巡检清单6.1 页面更新后用户仍看到旧版本index.html 的缓存策略try_files解决 404 之后另一个高频问题浮出水面每次发布新版本用户刷新页面看到的还是旧内容。原因通常是浏览器和 Nginx 都缓存了index.html而这个 SPA 入口文件恰恰不应该做长缓存。正确的策略是分而治之带 hash 的静态资源JS/CSS文件名每次构建都会变化可以放心长缓存设置expires 30d甚至1y都可以index.html则必须用no-cache保证每次刷新都能从服务器拿到最新版同时配合ETag做条件请求文件没变化时返回 304节省带宽。location /index.html { add_header Cache-Control no-cache, no-store, must-revalidate; expires -1; try_files $uri 404; } location / { try_files $uri $uri/ /index.html; }加一个精确匹配的location /index.html单独控制它的缓存头。这样index.html不会被缓存而其他静态资源该缓存缓存互不干扰。这个配置我几乎每个项目都会带上。6.2 刷新出现白屏但状态码正常路由守卫和异步组件的锅还有一类情况Nginx 配置完全正确接口也正常但深链刷新后页面白屏。这就要回到前端查找了。常见原因是全局路由守卫里做了权限校验刷新时会重新走一遍登录态判断如果异步获取用户信息的逻辑处理不当比如在未获取到状态时直接next(false)或者重定向到登录页用户看到的就是白屏或者闪一下又弹回首页。另外一个高发点是路由懒加载。刷新深链时按需加载的 chunk 会重新发起请求如果构建产物里某个 chunk 因为 CDN 回源失败或者文件路径拼错而 404页面会停在空白状态控制台报Loading chunk failed错误。这两类问题与 Nginx 无关但现象和最初的路由 404 很像排查思路要分清楚。我的习惯是先看 Network 面板里 HTML 请求是否是 200是不是index.html再看有没有 chunk JS 的 404最后看控制台有没有路由守卫相关的报错。按这个顺序能迅速定位问题出在服务端还是前端。6.3 一盏灯照完我留下的巡检清单多次踩坑之后我给自己总结了一份部署巡检清单每次上线前过一遍能省掉大量不必要的应急排查确认dist产物中的index.html和静态资源都存在资源引用路径是否符合子路径部署需求。检查 Nginx 配置中location /的回退是否只影响文档请求API 和静态资源是否被独立 location 处理。手动测试根路径、二级路由、深层路由、刷新、前进后退这五类场景Network 面板里不应出现预期外的 404。检查index.html的响应头确认Cache-Control不含长缓存带 hash 的静态资源可以长缓存。nginx -t通过后再reload发布窗口内尽量避开业务高峰。如果项目是多子路径部署逐个验证每个子系统的资源前缀、路由 base 和回退路径是否对齐。这份清单里的每一条几乎都能对应到某个真实线上事故。把这些琐碎的检查变成习惯之后路由 404 这类问题基本在还没被人感知之前就已经被挡在门外了。说回到开头那个项目最后我就是在 Nginx 里补了一行try_files再让运维 reload 了一下问题当场消失。但真正让我觉得有价值的是后面花了一晚上把原理、边界和子路径逻辑全部理清楚。如果你也遇到同样的 404希望这篇文章能帮你少走一些弯路而不是像我当年一样把 Vue 代码翻了整整两小时。