2353 字
约 7 分钟
18
S02-A18 · 刷新就 404:SPA 重定向与路由的那一处错配

单页应用在站内点击一切正常,用户按 F5 就 404。原因是服务器只配了 try_files $uri $uri/ /index.html 的"最后兜底",而 /orders/123 这种路径被当成"目录"处理,于是在兜底之前就先返回了 404。而开发环境用 historyApiFallback 自动兜住了,所以"本地一直好的"。

SPA 的 404 问题几乎全部来自同一处:服务器把"前端路由"当成"静态文件路径"。修法不复杂,但配错的方式有很多种,这类问题的排查成本主要花在"分清是前端还是后端"上:同一个 404,可能是前端路由没匹配,也可能是服务器没兜底,而两者在浏览器里长得一样。
而它们的表现相同。


1. 三种路由模式与它们的代价

模式 URL 形态 服务器要求
hash(#/orders) 带 # 无(全走 /)
history(/orders) 干净 需要"全部回退到 index"
memory(无 URL) 不变 不适用 Web

第二项是主流,代价是"服务器必须配合":任何未匹配静态文件的路径都要返回 index.html,并带 200 状态码。

而"带 200 状态码"这一条常被忽略:有些配置返回 index.html 但状态码是 404第二项(history)还有个"链接分享"的好处:URL 干净、可被复制。所以即使配置比 hash 麻烦,它也成了 SPA 的默认选择。
,于是页面能渲染而浏览器/爬虫认为是"不存在"。

2. 五种"看起来配了但没用"的写法

写法 问题
只对 / 回退 深层路径仍 404
回退到 /404.html 渲染空白页
回退带 404 状态码 页面能看但语义错
只对 index.html 做 rewrite 子路径未覆盖
忽略了 API 前缀 /api/* 也被兜到前端

第五项在"前后端同域"的部署里最容易发生:一个 location / 把全部路径都兜了,于是 /api/orders 返回 HTML,而前端报的是"JSON 解析失败"——完全看不出是路由问题。
第五项最危险:把 /api/ 也回退到 index.html,于是所有接口请求返回一段 HTML,前端拿到 Unexpected token '<' 这种看不懂的错误。

// 正确:先放行真实文件与 API,再兜底到 index
location / {
    try_files $uri $uri/ /index.html;     # 关键:最后一项兜底
}
location /api/ {
    proxy_pass http://backend/;           # API 不走前端兜底
}
// 注意:$uri/ 会把 /orders/123 当目录尝试,若目录不存在则落到兜底——
// 但若存在同名目录(如 /orders/),行为会不同;必要时去掉 $uri/

3. 静态资源为什么要单独处理

资源 风险
/assets/app.a1b2.js 若缺失,兜底返回 HTML → 白屏
/favicon.ico 返回 HTML → 控制台报错
/robots.txt 返回 HTML → 爬虫读到垃圾

第一项最典型:部署不完整或版本回滚时,某个旧 hash 的资源不存在第一项还有个"缓存"版本:用户浏览器里缓存了旧 index.html,它引用的资源在新版本里已删除,于是请求 404 → 兜底返回 HTML → 白屏。这类"只有老用户白屏"的问题,靠 index 的 no-cache 缓解。
,而兜底让它返回 index.html(Content-Type: text/html)——浏览器把它当 JS 解析,报"语法错误"。

所以资源路径应当"缺失就真的 404",而不是兜底。

// 资源目录:不做兜底(缺失就是 404)
location /assets/ {
    try_files $uri =404;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

4. 重定向与"尾斜杠"

场景 行为
/orders/ vs /orders 可能触发 301
前端路由匹配 尾斜杠可能导致不匹配
CDN 规范化 可能自动加/去斜杠

第二项在"多级路径"上更麻烦:/orders/123/ 与 /orders/123 都可能存在,而前端路由可能只定义了其中一种,于是"点进去正常、刷新就空"。
第二项在"路由定义严格匹配"时表现为"页面空白但无报错":路由是 /orders,URL 是 /orders/,两者不匹配,前端落到"未找到"分支。

第一项的风险在"重定向循环":服务器加斜杠、前端去掉斜杠,于是每次刷新都多一跳,甚至循环。

排查"这是不是兜底问题"有个十秒验证:直接在浏览器地址栏输入一个不存在的深层路径(如 /x/y/z)。如果返回首页而不是 404 页,说明兜底生效;如果返回 404,说明兜底没配到那一层。

// 三步定位(十秒)
1 访问 /x/y/z          → 404 说明服务器没兜底
2 访问 /api/health     → 返回 HTML 说明 API 被兜底
3 访问 /assets/不存在.js → 返回 HTML 说明资源被兜底(会白屏)
// 三个都正常 → 问题在前端路由匹配,不在服务器

5. 部署形态差异

形态 差异
本地 dev server 自带 fallback
静态托管(OSS/S3) 需配"错误页指向 index"
Nginx 需 try_files
CDN 前置 需在 CDN 侧也配

第二项的坑很具体:S3 静态托管的"错误文档"机制返回 index.html,但状态码是 404,于是 SEO 与监控都看到"404"。

第四项最容易被漏:Nginx 配好了,但 CDN 缓存了 404 响应,于是"修好了还有人报错"——因为缓存未失效。第四项还有个"缓存键"问题:CDN 若把"是否 404"也纳入缓存,则缓存里会长期存在那条 404;刷新时需要"按 URL 精确刷新",而不是"刷新整个目录"。

// 一次完整的"改完要清什么"
1 Nginx 配置 reload
2 CDN 刷新:/index.html 与 404 缓存
3 浏览器缓存:index.html 应设 no-cache
// 漏掉 2 或 3 → "我修了但用户还是 404"

"重写"与"重定向"必须分清,因为前者对用户不可见、后者会改 URL:

// 内部重写:URL 不变,用户看不到(推荐)
try_files $uri /index.html;

// 外部重定向:URL 会变(用户看到地址栏跳转,可能触发前端路由再次匹配)
rewrite ^/(.*)$ /index.html redirect;   # 通常是错的做法

6. 排错:五种"刷新就 404"

现象 成因 处理方向
深层路径 404 只配了 / 回退 用通配兜底
页面能看但 SEO 报 404 兜底返回 404 码 改 200
接口报"Unexpected token '<'" API 被兜到 index 排除 /api/
白屏且控制台语法错误 资源被兜底返回 HTML 资源目录 =404
修好了仍有人报错 CDN/浏览器缓存 刷新缓存 + no-cache

第二项还有个"监控"后果:404 率异常升高会触发告警,而它其实是"兜底配置错",于是排查方向被引向"是不是有坏链"——方向错了半小时。
第二项最容易被忽略,因为它"看起来正常":用户能操作,但搜索引擎与监控认为页面不存在。

最后一条"SEO"边界:兜底返回 200 会让"真实不存在的页面"也变成 200,于是搜索引擎可能收录一堆空页面。缓解方式是"前端在路由未匹配时渲染一个 404 内容并设置 <meta name="robots" content="noindex">"——状态码仍由服务器给,但内容不再被认为是有效页。

<!-- 前端 404 分支:内容层声明 -->
<meta name="robots" content="noindex">
<!-- 更彻底:对"已知不存在"的路径由服务器直接 404(不兜底) -->

7. 边界:什么情况不必配兜底

场景 为什么 做法
hash 路由(#/x) 无需服务器配合 不配
多页应用(MPA) 每页是真实文件 不配
内部工具(用户不刷新) 风险低 可省
已用框架的 SSR 服务端渲染路由 不配

判断口径一句话:URL 里有没有"不对应任何静态文件"的路径。最后一条与"部署"有关的边界:兜底规则要写进部署文档,因为它是"服务器与前端之间的隐性契约"。缺了它,前端换路由模式时会静默坏掉,而没人知道该改哪里。
有(history 模式),就必须配兜底;没有(hash 或 MPA),配了反而会掩盖真实 404。


8. 自测

  1. 三种路由模式里,哪一种需要服务器配合?"带 200 状态码"为什么必须?
  2. 五种"看起来配了但没用"里,哪一种会让前端拿到 Unexpected token '<'?
  3. 静态资源为什么应当"缺失就真的 404"?兜底会造成什么可观测现象?
  4. 尾斜杠不一致会怎样?"重定向循环"怎么产生?
  5. 为什么"修好了仍有人报错"?完整修复要清哪三处?
  6. 判断"是否需要配兜底"的那个问题是什么?
S02-A18 · 刷新就 404:SPA 重定向与路由的那一处错配
https://www.clxhxhhr.top/posts/6537/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。