history 模式上线后深链接刷新 404:Nginx 回退、/api 被吞与 base 配错的排查
现象
本地 dev 一切正常,页面内点链接跳转也正常。上线后把详情页链接发给同事,对方直接打开就是 404;自己在详情页按 F5,同样 404。同一个团队里用 hash 模式的内部后台,从来没出现过这个问题。
这类问题不是路由写错了,而是「服务器不认识前端路由」。下面按请求链路拆开看。
机制
hash 与 history 的请求差异
hash 模式(Vue Router 4 的 createWebHashHistory,Vue Router 3 的 mode: 'hash' 是默认值)里,# 之后的部分不会进入 HTTP 请求。服务器永远只看到 /index.html,所以不可能 404。代价是 URL 带 #、服务端不知道用户访问的是哪个路由因而做不了 SSR、referer 头不带 hash、SEO 差。
history 模式(createWebHistory)用 pushState/replaceState 改地址栏,用 popstate 响应前进后退。URL 干净,但深链接被直接访问时,服务器必须懂得把 /user/123 这类路径回退到 index.html。
参考:Vue Router — HTML5 History 模式
Nginx 回退
location / {
try_files $uri $uri/ /index.html;
}
几个容易踩的点:
$uri/会让真实存在的目录命中目录索引。目录下没配index时会给 403 或 autoindex 列表,不会走到回退。SPA 项目里通常只留$uri和/index.html:location / { try_files $uri /index.html; }顺序:
try_files之前的location精确匹配优先,所以/api要单独写location,且不能落到回退里。不要用
rewrite . /index.html这类写法,它会把 POST body 丢掉或把请求改成 GET。回退生效后,所有不存在的路径都返回 200,服务器不再报 404。前端必须补 catch-all 路由,并放在路由表最后(Vue Router 3 是
path: '*'):{ path: '/:pathMatch(.*)*', component: NotFound }
/api 被回退吞掉
典型症状:前端 fetch('/api/user') 报 Unexpected token '<', "<!DOCTYPE"...,或 JSON.parse 失败。原因是 /api/user 不存在时被回退成 index.html 且状态码 200,请求「成功」了,拿到的是 HTML。
排查:
curl -i https://site/api/xxx
看 content-type 是不是 text/html。修法是给 api 前缀单独开 location,不让它进回退:
location ^~ /api/ {
proxy_pass http://backend;
}
或者在 try_files 中对 api 前缀单独 =404。
子路径 base
部署在 https://example.com/admin/ 下时,两处都要改:
// vite.config.js
export default { base: '/admin/' }
// router
createWebHistory('/admin/')
只改一边的现象:路由跳转变成了 /user 而不是 /admin/user;或者 index.html 里的 /assets/index-xxx.js 404,因为资源被打到了根路径。
相对路径 ./ 在 history 深链接下会按当前路由路径解析,容易出错,所以 Vite 默认输出绝对路径。不要为了兼容子路径去手改成 ./。
静态托管与 CDN
S3/OSS 静态托管没有 try_files,要把错误文档(404/403)指向 index.html。
Cloudflare Pages、Netlify 用 _redirects 或 netlify.toml:
/* /index.html 200
必须写 200,写 301 会丢 POST,地址栏还会跳一下。
index.html 必须 no-cache 或短缓存。否则新版本发布后,旧 index.html 还引用已被清掉的 assets/index-<旧hash>.js,浏览器报 Failed to load module script 或直接白屏。assets 目录可以长缓存。
分型排查
按症状对号入座,比从头读配置快:
| 症状 | 大概率原因 |
|---|---|
| 深链接和新开窗口 404,页面内跳转正常 | Nginx/托管平台缺回退 |
| 只有子目录部署时才 404,资源也 404 | base 与 createWebHistory() 只改了一边 |
接口报 Unexpected token '<' | /api 被回退成 HTML,需单独 location |
| 部署在 S3/OSS 上,刷新即 404 | 错误文档没指向 index.html |
| 发版后白屏,报 module script 加载失败 | index.html 被缓存,引用了旧 hash 资源 |
| 回退生效了,但乱输路径返回 200 空白页 | 缺 catch-all 404 路由 |
上线前自检清单
# 1) 直接访问一个深链接,期望 200 + text/html
curl -I https://site/user/123
# 2) 访问一个 /api 路径,期望 JSON 或 4xx/5xx,不是 HTML
curl -i https://site/api/xxx
- 检查返回的
index.html里资源路径是否带正确的 base 前缀; - 刷新、直接粘贴链接、后退,各试一次;
- 故意访问一个不存在的路由,确认前端 404 页出现。
模式选择的边界
面向内部、菜单驱动的后台,hash 模式省掉一整套服务端约束,成本更低;面向公开流量、需要分享链接和 SEO 的站点,history 加回退是必须付的配置成本。别在同一个域名下混用两种模式。