ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

前端路由History与Hash模式区别及刷新404的Nginx配置方案

前端路由History与Hash模式区别及刷新404的Nginx配置方案 上周一个同事跑来找我说线上项目的路由在刷新后白屏运维自查了半天最后发来一句话你们这个是不是叫History模式Nginx少了一段配置。我下意识看了一眼 router 代码果然用的是 createWebHistory()。这其实是我遇到频率最高的一类前端部署问题开发环境跑得好好的一上线用户打开首页没问题点菜单也正常但只要F5一刷新或者有人把链接收藏起来第二次打开就404。这类问题十有八九是History模式和Hash模式的区别没搞透后端回退配置没跟上。这篇文章我打算按自己动手的方式把History模式和Hash模式的底层区别、前端配置方法、服务器端的配套改动串起来讲一遍。无论你是刚学 Vue Router 或 React Router还是已经在线上项目里踩过这一脚应该都能找到可以直接抄的配置方案。1. 两种模式的路由匹配机制差异藏在这些底层细节里1.1 URL形态背后的行为差异两种模式最直观的区别看地址栏就够了。Hash 模式下URL 长这样https://example.com/#/user/listHistory 模式下URL 长这样https://example.com/user/list。很多教程只讲到一个带井号一个不带井号就停了但真正决定你部署方式差异的不是井号本身而是浏览器对这两类 URL 的不同处理方式。先看 HTTP 协议层的一个冷知识#以及它后面所有内容根本不会发送到服务器。你访问https://example.com/#/user/list时浏览器发到服务器的请求路径其实还是/。服务器拿到的一直是根路径SPA 的 index.html 自然每次都能兜住。前端只需要监听hashchange事件当用户点击路由、手动改 hash 的时候把 hash 部分解析成对应的路由并渲染组件。整个链路里服务器不需要做任何和路由相关的配置因为服务器永远只响应根路径的请求。History 模式则完全不同。它基于 HTML5 的 History API核心是pushState和replaceState这两个方法。这两个方法可以修改地址栏 URL、往历史记录里插入条目但不会触发浏览器向服务器发起请求。单看这一步前端确实做到了无刷新改 URL。可问题在于当你刷新页面、或者用户直接在地址栏输入https://example.com/user/list时浏览器不知道这是 SPA 的虚拟路由它会按真实路径去请求/user/list。如果服务器上没有这个目录也没有配置回退规则就直接返回 404。所以 History 模式不是不需要服务器而是把服务器要做的事情从不用管变成了必须管。1.2 History API 里容易搞错的一个细节很多人以为前端路由是纯靠popstate事件驱动的这个理解其实不完全对。pushState和replaceState被调用后浏览器不会主动触发popstate事件popstate只在用户点击浏览器前进/后退按钮、或者调用history.back()这类方法时才触发。那框架是怎么感知到路由变化的答案是 Vue Router 和 React Router 内部都对原生的 History API 做了包装在调用pushState之后手动触发自身的响应机制。这也是为什么你可以在业务代码里直接调用router.push()而不需要关心底层事件的原因。明白了这一点你就知道为什么排查 History 模式问题时不能只盯着前端代码看——刷新这种事框架拦不住请求已经发到服务器了。1.3 Hash 模式也有自己的坑Hash 模式虽然配置省事但它有自己的麻烦。#后面的参数不会出现在服务端日志里埋点系统如果依赖服务端日志收集访问路径拿到的数据天然缺一段。微信授权、支付宝回调这类场景里回调地址如果带着 hash解析授权码时会遇到各种诡异问题。hash 模式对 SEO 极度不友好搜索引擎爬虫对#后面的内容收录效果很差。页面上如果还要做锚点定位#/user/list这种 hash 路由和#section锚点会冲突处理起来很费劲。所以 Hash 模式在绝大多数新项目里其实是妥协方案不是因为好用而是因为它环境要求低。只不过很多项目在选型阶段没人说清楚这件事导致后面想切回 History 模式时才发现服务器配置跟不上。1.4 两种模式核心差异速查表对比维度Hash 模式History 模式URL 示例example.com/#/user?id1example.com/user?id1刷新时请求的路径始终是/不含 hash真实请求/user?id1服务器是否需要回退配置不需要必须配置try_files / fallback底层依赖事件hashchangepushState/replaceState/popstateSEO 友好度差较好仍需 SSR 或预渲染配合分享链接美观度带#能用工整接近真实 URL服务端日志可见性看不到 hash 后的路径能看到完整路径开发环境切换成本低低但部署成本高2. 配置落地前端路由声明与服务器回退规则2.1 Vue Router 4 的两种写法以 Vue 3 Vue Router 4 为例切换两种模式只需要改一行。// 方式一Hash 模式 import { createRouter, createWebHashHistory } from vue-router const router createRouter({ history: createWebHashHistory(), routes }) // 方式二History 模式 import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes })createWebHistory()可以接收一个 base 参数比如站点部署在https://example.com/app/下就写成createWebHistory(/app/)这个 base 参数要和打包配置的 publicPath 保持一致否则资源路径和路由路径会错位。React Router 6 的写法也差不多import { createBrowserRouter, createHashRouter, RouterProvider } from react-router-dom // History 模式 const router createBrowserRouter(routes) // Hash 模式 const router createHashRouter(routes) export function App() { return RouterProvider router{router} / }2.2 Nginx 配置 History 模式的完整示例这是线上最常见、也最容易出问题的一步。先说结论只要用 History 模式Nginx 必须有try_files回退。server { listen 80; server_name your-domain.com; root /var/www/html; index index.html; location / { try_files $uri $uri/ /index.html; } # 可选处理带版本号的静态资源缓存 location /assets/ { expires 30d; add_header Cache-Control public, no-transform; } }重点解释一下try_files $uri $uri/ /index.html;这行的执行逻辑先按请求路径找真实文件比如请求/favicon.ico如果文件存在就直接返回。找不到文件就找目录比如请求/about时尝试找/about/目录下的 index.html。前两步都不满足就把请求回退到/index.html由前端路由接管。这个回退规则解决的就是用户直接访问深层路由的问题。没有这一步Nginx 会老老实实去磁盘找/user/list对应的物理文件找不到就返回 404。很多线上故障的根因就是try_files这一行没写。如果你发现项目在开发环境用npm run dev一切正常部署到 Nginx 后刷新 404先去查这一行在不在。2.3 子路径部署的配置如果项目部署在https://example.com/app/这种子路径下配置要跟着改四处少一处都会出问题。第一处打包配置里设置 publicPath// vite.config.ts export default defineConfig({ base: /app/ })第二处路由实例里设置 basecreateWebHistory(/app/)第三处Nginx 的 location 要调整注意 alias 和 try_files 的配合location /app/ { alias /var/www/html/; try_files $uri $uri/ /app/index.html; }第四处router 里的跳转路径要统一。写死/user的地方要改成/app/user或使用全局前缀否则刷新后路由和资源对不上。这四处配置看着琐碎但子路径部署出问题的概率极高建议按这个清单逐项检查。2.4 其他服务端环境的兜底写法如果你不用 NginxNode.js 环境可以用现成的库// Express connect-history-api-fallback const history require(connect-history-api-fallback) const express require(express) const app express() app.use(history()) app.use(express.static(dist)) app.listen(3000)connect-history-api-fallback会在请求不匹配任何静态文件时把请求回退到index.html。注意中间件顺序必须先挂 history再挂静态文件服务否则静态资源请求会被误回退。Spring Boot 项目里如果直接把前端 build 后的文件放进src/main/resources/static做法是在安全配置或拦截器里放行/index.html同时实现一个 forwarding controllerController public class SpaForwardController { RequestMapping(value {/user/**, /list/**, /detail/**}) public String forward() { return forward:/index.html; } }思路就是对非接口、非静态资源的路径统一转发到 index.html。实际项目里路径可能很多建议用 Filter 处理而不是手写一堆映射。Apache 也有对应配置但国内实际生产环境 Nginx 占绝对主流这里不展开。3. 线上刷新404与空白页一次完整的故障排查链路3.1 先确认问题边界拿到报障第一件事不是改代码而是先问清楚三个问题是刷新后 404还是页面渲染出来但资源全挂开发环境是否正常线上用的是 Nginx、CDN、还是一些带验证的托管平台这三个问题能过滤掉一半的干扰项。刷新后 404 说明服务端回退配置缺失或错误页面能出来但样式错乱、控制台全是资源加载失败那多半是资源路径问题开发环境正常而线上异常基本可以排除路由表本身写错的可能。3.2 用 curl 验证服务端到底返回了什么不用 curl 猜直接看服务器响应最靠谱。curl -I https://example.com/user/list如果返回404 Not Found说明服务器没有把/user/list回退到 index.html问题出在 Nginx 或后端配置。如果返回200 OK但Content-Type是text/html说明回退已经生效问题可能在前端资源路径或路由匹配。如果返回200 OK但内容是接口的 JSON说明匹配到了后端接口而不是页面要查路由优先级。再配合浏览器 Network 面板刷新一次看 HTML 文档请求和静态资源请求的路径前缀是否一致基本能定位到根因。3.3 白屏排查静态资源相对路径的坑有一种情况是服务器返回了 index.html页面内容结构也在但 CSS 和 JS 一个都没加载出来控制台全是 404。这种大多是打包后的资源路径用了相对路径。History 模式下当你位于/user/list时浏览器解析相对路径会以/user/为基准。比如 index.html 里写的是./assets/main.js它会去请求/user/assets/main.js这个路径在服务器上根本不存在。解决办法是把资源路径改为绝对路径在打包配置里设置publicPath: /或base: /。子路径部署场景则要设置成/app/这种完整前缀。3.4 try_files 配了但依然 404 的几种情况配置了try_files但问题依旧存在十有八九是下面这些场景现象可能原因排查方向首页正常深层路由刷新 404try_files 没写或写错检查 Nginx location 部分首页正常深层路由刷新返回 Nginx 默认 404 页子路径部署时回退路径写错回退路径是否带了/app/前缀刷新后返回后端接口 404 错误页后端网关拦截了非 API 请求在网关层放行前端路由或修改路径前缀白屏但 HTML 是正常的静态资源相对路径导致加载失败检查打包 publicPath / base首页直接访问就 404root 路径配置错误或端口不通检查 root 指向、index 配置、防火墙某些路由刷新正常另一些刷新 404该路由被后端当作真实接口处理检查后端拦截器路径规则一条条对应排查通常能在十分钟内定位问题。我最常遇到的其实是子路径部署回退路径写错和后端网关拦截了非 API 请求这两种因为它们从纯前端视角完全看不出来。3.5 验证修复是否到位改完配置别急着收工至少做三件事验证页面内点击路由到深层路径刷新确认 200。在地址栏直接输入深层路径回车确认 200。用 curl 访问一个不存在的静态资源路径确认它会回退到 index.html 而不是 404如果确实需要返回 404 的静态资源要在 try_files 里单独处理。这三件事能覆盖用户实际会遇到的 95% 的路径访问场景。4. 配置完成后到底该选哪种模式4.1 适合 Hash 模式的场景Hash 模式不是过时的技术它有自己的舒适区。如果你的项目属于下面几类用 Hash 模式反而省心部署在 GitHub Pages、OSS 静态托管、CDN 回源只有静态文件这种环境没有条件改服务器规则。内网后台管理系统用户量小不需要 SEO链路越短越好。客户给的环境是一台老服务器Nginx 版本老、权限控制严格连 try_files 都不敢随便改。项目生命周期短快速上线验证后续大概率不会做 SSR 或预渲染。在这些场景里Hash 模式的核心优势是零后端配置不容易因为环境差异出问题。4.2 适合 History 模式的场景反过来如果你满足下面任意两条建议优先考虑 History 模式服务器是自己可控的能修改 Nginx 或后端配置。有 SEO 需求哪怕现在没有未来也大概率会有。分享出去的链接要求美观不想让人看到一串#。需要对接微信登录、支付回调这类对 URL 形态敏感的服务。项目可能演进到 SSR 或静态预渲染。History 模式下URL 和真实请求路径一致调试、日志分析、后端统计都更顺手。4.3 进阶注意动态路由、SSR 和路由守卫切到 History 模式后动态路由会有个隐藏坑。如果项目用router.addRoute()动态注册路由刷新页面时需要重新从接口拉取权限数据、重新注册路由再replace到目标路径。这个流程没做好会出现刷新后能进入页面但组件渲染空白或刷新后直接落到 404 页面的情况。排查时别只在 Nginx 上死磕也要看动态路由恢复的逻辑。另外如果项目未来打算做 SSR服务端渲染Hash 模式基本不适合。大部分 SSR 框架的约定都是基于真实路径的Hash 模式在服务端无法预取数据。所以做技术选型时这一点要提前想清楚别等架构定了再翻车。4.4 我自己的选型习惯我个人的习惯是只要能拿到服务器配置权限默认用 History 模式如果客户给的是纯静态托管环境或者内网老系统直接用 Hash 模式省得后续为运维环境扯皮。项目里如果确定要做 SSR直接放弃 Hash因为 SSR 框架的生态基本都是围绕 History 设计的。另外在 Nginx 里配置时建议单独写备注说明 try_files 的作用不然下次换运维或换服务器这段配置可能被人优化掉又复现刷新 404。这种事故我见过不止一次了。
返回列表