history 模式 vs hash 模式 · 谁刷新会 404
SPA 在浏览器里管路由,前端怎么把「当前路由」写进地址栏,有两套写法。它们看起来只是 URL 长得不同,真正的分水岭是一个底层事实:URL 里 # 后面那段 (fragment / hash) 不会被发送给服务端。这一个事实,决定了两种模式在「直接打开 / 刷新一个深层 url」时命运完全不同。
下面让两台浏览器并排跑同一条内部路由:左边 history 模式(干净 URL /products/42)、右边 hash 模式 (/#/products/42)。切换内部路由、按「此刻刷新」,看服务端分别收到什么请求、结果是 200 还是 404。
关键就一句:hash 后面那段不上服务端。 hash 模式刷新任何深层 url,浏览器实际只向服务端要 GET /——永远命中 index.html、永远 200,深层路由 (#/products/42) 全在前端读 location.hash 自己解。history 模式则把 /products/42
原样发给服务端,服务端路由表里根本没有这条——不配 fallback 就 404。勾上 fallback 开关,看 history 模式怎么恢复 200。
1 · history 模式为什么默认更受欢迎,代价是什么
干净的 /products/42 对用户可读性、SEO、分享都更友好(没有那个多余的 #),所以现代 SPA 默认都用 history 模式。代价只有一条:服务端必须配一句 fallback rewrite——任何没被 /api/* 等真实路由接住的 path,一律 rewrite 到
index.html,把路由判断交还给前端 router。这正是 分享入口页讲 SPA 接入时的第三条服务端路由 (* fallback)。
前端两种模式怎么读「当前路由」,差别就在这几行——history 用 pathname + popstate,hash 用 location.hash + hashchange:
// history 模式: 路由写在 path 里, 用 pushState 改, 监听 popstate
function currentRoute() { return location.pathname + location.search; }
addEventListener('popstate', () => router.render(currentRoute()));
router.navigate = (to) => { history.pushState(null, '', to); router.render(to); };
// hash 模式: 路由写在 # 后面, 改 location.hash 即可, 监听 hashchange
function currentRoute() { return location.hash.slice(1) || '/'; } // 去掉开头的 #
addEventListener('hashchange', () => router.render(currentRoute()));
router.navigate = (to) => { location.hash = to; }; // 浏览器自动发 hashchange
服务端这边,history 模式必须的就是那句 fallback(以 nginx try_files 为例):
# history 模式: 真实接口照常走, 其余「文件不存在」的请求一律回 index.html
# —— 把路由判断交还给前端 router (这就是 SPA fallback)
location /api/ { proxy_pass http://backend; } # 真实接口: 反代给后端
location / {
try_files $uri $uri/ /index.html; # 找不到对应文件 → 回 index.html
}
# hash 模式不需要这句: 浏览器永远只请求 "/", 服务端给 index.html 就够了。
那 hash 模式什么时候反而是对的? 当你没有、或不想配置服务端的时候——纯静态托管(GitHub Pages、对象存储)、Electron / 本地 file:// 打开、或一个嵌在别人页面里的小工具。这些场景下「让服务端给每条深链都回 index.html」要么做不到、要么不划算,hash
模式零服务端配置就能跑,是它今天唯一的、但很实在的存活理由。
2 · 相关页
- 本系列 · 客户端路由——history 模式改地址用的就是该页那个
pushState;本页只是追问「改完之后、刷新一下会怎样」。 - 本系列 · 分享地址统一入口——那页讲 SPA 接入时服务端只认三条路由,其中第三条
*fallback rewrite,正是 history 模式能成立的前提。 - URL Anatomy · 一条网址怎么拆开——为什么
#fragment不上服务端?那个系列把 URL 逐段拆开,fragment 的「只在客户端」语义讲得最透。