资讯动态

Vue Router 路由模式详解:hash 与 history 的底层原理及 Nginx 配置

发布时间:2026/9/16 18:06:48 来源:尧图企业网站定制
很多人第一次从 Vue 的入门教程切换到实战项目时基本都会在路由模式上卡一下。hash模式是脚手架默认给你配好的跑起来也没毛病等部署上线才发现 URL 里带着#有些场景下越看越别扭。于是你打开文档看到history模式改一行代码本地一切正常部署到 Nginx 之后一刷新就是一个 404。这个场景我见过太多次了包括我自己早期也这么翻过车。这篇就专门把 Vue Router 的这两种模式——hash和history——彻底讲透。我会从浏览器的底层机制讲起结合实际的 Nginx 配置和开发环境配置把每种模式能做什么、不能做什么、适合什么时候用全部摊开来说。无论你是刚接触 Vue 的新手还是写过几个项目但一直没搞懂原理的开发者读完这篇应该都能做到心里有底。1. 两种模式的本质区别URL 形态与路由原理很多人以为hash和history的区别只是 URL 里有没有#这个认知太表面了。这两者在底层的实现机制、浏览器 API 的调用方式、以及服务器配置要求上差异非常大。1.1 hash 模式利用锚点特性实现的前端路由hash模式的 URL 长这样https://example.com/#/user/123这里的#/user/123是 URL 中的 hash 部分也叫锚点。在浏览器里改变 hash 值不会触发页面重新加载#及后面的内容也不会被发送到服务器。这就意味着无论你怎么改动#后面跟着的路由路径服务器收到的请求永远都是https://example.com/服务端不需要做任何额外配置。Vue Router 在hash模式下核心监听的是浏览器的hashchange事件。当用户点击路由链接、或者手动在地址栏修改#后面的内容时hashchange被触发Vue Router 就能拿到变化后的 hash 值去匹配对应的组件并渲染出来。这个机制有一个很关键的细节值得注意hash模式的 URL 变化本质上是修改了当前页面的锚点位置。因为浏览器传统的#锚点行为是跳转到页面中id对应的元素位置而 Vue Router 的 hash 并不是真实的元素 id所以它不会产生页面滚动的副作用只是纯粹的“地址变化”信号。这个特性让hash模式天然适合纯前端路由的场景因为不管怎么折腾页面本身是从一个 HTML 入口加载的刷新也只是重新加载这个入口文件路由解析完全在浏览器端完成。1.2 history 模式基于 History API 的“真正”路径history模式的 URL 长这样https://example.com/user/123干净、漂亮跟正常的多页应用 URL 长得一模一样。这个模式底层用的是 HTML5 的 History API核心方法是history.pushState()和history.replaceState()。这两个方法可以改变浏览器的地址栏 URL同时把这个新地址压入历史栈但不会向服务器发送任何请求也不会触发页面的重新加载。Vue Router 在history模式下监听的是popstate事件。popstate跟hashchange有一个很大的区别pushState和replaceState本身不会触发popstate触发popstate的事件只有用户点击浏览器的前进/后退按钮、或者调用history.back()、history.forward()、history.go()这类操作。所以 Vue Router 内部需要自己包装一下路由变化逻辑当你在代码里调用router.push()时Vue Router 内部调用pushState改变地址然后自己匹配路由更新视图当用户触发前进或后退时popstate事件告诉 Vue Router 地址变了需要重新匹配并渲染视图。这里还牵扯到一个很多初学者会搞混的概念history模式的场景会在用户直接访问一个深层路径比如直接在浏览器输入https://example.com/user/123或者点击刷新时向服务器发出真正的 HTTP 请求。服务器拿到这个请求之后如果找不到对应的静态文件就会返回 404。这就是所谓“history 模式必须配合服务器配置 fallback”的原因。我先把这个关键差异摆在这里后面第 3 节会展开讲具体怎么配置。1.3 从一次路由跳转看两种模式的完整流程差异我用一个具体的场景来说明。假设当前地址是首页https://example.com/用户点击了一个跳转到“用户详情页”的按钮路由目标路径是/user/123。在hash模式下浏览器地址栏会变成https://example.com/#/user/123。这个变化是通过修改location.hash实现的整个过程中浏览器没有发出新的 HTTP 请求页面也没有刷新hashchange事件触发Vue Router 接管并渲染新组件。在history模式下浏览器地址栏会变成https://example.com/user/123。这个变化是通过history.pushState()实现的同样不触发页面刷新不发 HTTP 请求。但注意Vue Router 内部的router-link组件会拦截链接的默认跳转行为阻止浏览器真正向服务器发请求然后手动调用pushState完成 URL 变更再走路由匹配逻辑。两者的核心差异在于“刷新”这个动作。hash模式下刷新浏览器请求的是https://example.com/服务器返回了index.html里面的 JS 读取到location.hash为/user/123于是正确渲染出详情页。history模式下刷新浏览器请求的是https://example.com/user/123服务器如果没有对应的静态文件或 fallback 配置就直接 404。2. hash 模式深入拆解为什么它开箱即用却总被嫌弃刚接触 Vue 时大部分脚手架都默认用hash模式因为它确实省心。但这几年很多项目在演进过程中纷纷转向了history模式。要理解这种趋势得先把hash模式的机制细节和应用边界看清楚。2.1 hash 的天然屏障URL 中的#到底影响了什么hash模式被诟病最大的就是 URL 不好看。对于面向用户的 C 端产品来说URL 是产品的一部分。一个分享出去的链接长这样https://shop.example.com/#/product/8899无论从视觉还是品牌层面都显得不够专业。但影响不只是颜值。hash模式对搜索引擎的索引是不友好的。虽然 Google 现在官方说能够处理 hash 路由但实际索引的优先级、深度和效率相比history模式的“普通 URL”要弱很多。对于需要依赖搜索引擎自然流量的站点这会是一个隐患。还有一个很容易被忽略的问题很多第三方服务的回调 URL 里如果带了hash可能会出问题。比如支付回调服务商允许配置的回调地址如果被你填成https://example.com/#/payment/callback部分支付服务商会把#后面的内容截断或忽略导致回调参数丢失业务逻辑断了。这种事情在真实项目里发生过排查起来也挺隐蔽。2.2 hash 模式的工作机制与 vue-router 源码细节想要真正理解hash模式可以看看 Vue Router 3.x 的源码实现。它的核心逻辑可以简化为// 简化版 hash 模式路由实现 window.addEventListener(hashchange, () { const currentHash window.location.hash.slice(1) || / // 根据 currentHash 匹配路由表渲染对应组件 matchAndRender(currentHash) }) function push(target) { window.location.hash target // 注意这里不是立即调用 matchAndRender // 因为 location.hash 的修改会触发 hashchange 事件 // Vue Router 内部正是依赖 hashchange 回调来完成视图更新 }这里有一个重要的时序问题当你给window.location.hash赋值时浏览器会异步触发hashchange事件。所以Vue Router里的路由跳转 API如router.push()在hash模式下本质上只是同步修改了location.hash真正的视图更新是在事件回调里完成的。有一点需要特别提防当用户手动在地址栏修改 hash 时浏览器会同时触发两个事件一个是hashchange另一个是页面的popstate在部分浏览器中。Vue Router 内部有防重逻辑会在路由变更时报错“duplicate navigation”之类的提示。如果你在开发自己的路由实现时要注意规避这种重复触发的问题。实际使用中我踩过一个相关的小坑在hash模式下同时监听hashchange和popstate会造成部分场景下路由跳转被触发两次。如果你用 Vue Router 这把官方提供的“成品”就不用担心这个问题但如果自己造轮子必须留意。2.3 hash 模式在部署和运维上的优势说完了缺点回头聊聊hash模式的“舒适区”。最核心的一条已经提过很多次了——不需要服务端配置。你不需要动 Nginx不需要配置 fallback只需要保证所有请求都返回同一个index.html就行。这在以下场景中非常重要静态资源托管GitHub Pages、对象存储OSS/COS/S3 CDN这些都是纯静态托管不支持 rewrite 规则hash模式几乎是唯一方便的选择。服务端不方便配置的环境某些企业内网环境、客户自建服务器的场景你可能没有权限改 Nginx 配置或者改了容易出问题hash模式帮你规避了沟通成本。临时 Demo、活动页面快速搭一个原型或活动页没有复杂路由用hash模式就够了没必要折腾服务器配置。另外注意一点在hash模式下hash部分的变化是去不掉浏览器历史记录的。每次修改location.hash都会往历史栈中压入一条记录用户点击后退按钮会按 hash 变化顺序回退这个行为大家感受得到比如在 hash 路由的页面上点击多个详情页之后再点浏览器后退键会按你点击的顺序一级级退回而不是直接跳出应用。这个体验其实跟history模式差不多因为history模式里pushState也会压入历史栈除非你使用replaceState替换当前记录。3. history 模式从原理到实践前端一味爽快后端要跟上history模式在开发环境和部署环境是两个完全不同的世界。很多人本地跑npm run dev一切正常一部署到测试环境就 404根源就在开发服务器和生产服务器对“未知路径”的处理策略不一致。3.1 开发环境配置为什么你的 npm run dev 能自动支持 history 路由本地开发时用的 Webpack Dev Server或者 Vite Dev Server在收到浏览器的路由请求时会先尝试匹配磁盘上的物理文件匹配不到的时候它不会直接返回 404而是检查配置项historyApiFallback。默认情况下Vue CLI 和 Vite 创建的 Vue 项目里这个选项都是开启的所以开发环境下history路由刷新不会 404。Vite 里对应的配置项在vite.config.js中export default defineConfig({ server: { // 开发服务器默认已开启 history fallback通常不需要额外配置 // 但如果你的项目路径比较特殊可以显式指定 historyApiFallback: true, }, })Webpack Dev Server 中对应的配置module.exports { devServer: { historyApiFallback: true, }, }这个配置的本质是把所有无法匹配静态文件的 GET 请求都重写为返回index.html。因为前端路由本质上只有一个 HTML 入口浏览器加载的永远是同一个index.html具体的页面内容由 Vue Router 加载 JS 后根据当前 URL 动态渲染。这就是“SPA 前端路由”的核心逻辑——服务器只负责吐出一个 HTML 壳子剩下的路由逻辑全部在浏览器端处理。但这里有个大坑historyApiFallback面对的是“所有未知路径”它不会区分这个路径是路由路径还是真实的静态资源路径。如果public目录下放了favicon.ico、robots.txt、static文件夹里的图片等文件这些路径能正常匹配到物理文件没问题。但如果你在public目录下放了一个old-page.html而路由表里也有一个/old-page的路径浏览器访问https://localhost:8080/old-page.html时会直接返回静态文件访问https://localhost:8080/old-page时返回index.html。两者内容完全不一样极容易造成迷惑。生产环境里也有同样的风险后面会再提到。3.2 生产环境 Nginx 配置try_files 的完整解析部署到 Nginx 时你需要显式配置 fallback。最常见也最推荐的写法是这样的server { listen 80; server_name example.com; root /var/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } # 静态资源缓存策略可选 location ~* \.(js|css|png|jpeg|jpg|gif|svg|webp|ico|woff2?)$ { expires 30d; add_header Cache-Control public, no-transform; } }try_files的匹配过程分为三步先按$uri尝试找对应的物理文件。比如https://example.com/user/123Nginx 会检查/var/www/dist/user/123这个文件是否存在。如果文件不存在再尝试$uri/也就是把路径当作一个目录检查是否存在/var/www/dist/user/123/这个目录。如果目录也不存在就回退到/index.html也就是把/var/www/dist/index.html这个文件作为响应返回给浏览器同时返回 HTTP 状态码 200。最后一步是关键。不管 URL 是什么只要 Nginx 找不到对应的物理文件它都会返回index.html。浏览器拿到 HTML 之后加载 JSVue Router 再根据当前的window.location.pathname去匹配路由正确的组件就会被渲染出来。整个过程虽然地址栏里是/user/123但服务器实际返回的文件是index.html所以这个机制也叫“SPA fallback”。3.3 从 Nginx 到云服务商不同环境下的 history 模式配置实际工作中很少人能直接改 Nginx 的 conf 文件。更多的场景是对象存储阿里 OSS / 腾讯 COS / AWS S3这些纯静态托管有时候不能配 rewrite但有些支持“错误文档/自定义 404 页面”。把自定义 404 页面设置为index.html在部分场景下也能实现 fallback但这种方式有它的限制返回的 HTTP 状态码可能是 404而不是 200。这会带来两个问题——一是浏览器开发者工具里看到红字排查问题时有干扰二是 SEO 上不友好搜索引擎可能不索引 404 页面。所以如果对 SEO 有要求的项目建议还是用支持自定义 rewrite 或者说支持“托管规则”的托管商要么干脆放到云服务器上用 Nginx。如果你用的是 Express 或 Koa 这类 Node 服务生产环境同理需要做 fallbackconst express require(express) const path require(path) const app express() // 静态资源 app.use(express.static(path.join(__dirname, dist))) // SPA fallback app.get(*, (req, res) { res.sendFile(path.join(__dirname, dist, index.html)) }) app.listen(3000)在 Express 里有个细节需要注意app.get(*, ...)的*是字符串通配符在 Express 4 中匹配所有路径。但如果你用了path-to-regexp的语法写app.get(/*, ...)反而会匹配不到根路径。所以稳妥起见直接写app.get(*, ...)。如果是 Express 5*的写法发生了变化需要使用app.get(/*splat, ...)之类的通配语法或者用中间件app.use((req, res) res.sendFile(...))来做 fallback这样最稳妥不受版本影响。还有一个容易踩坑的地方如果你在 Nginx 配置了接口反向代理比如location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }那么/api/开头的请求不会匹配到location /的try_files规则因为 Nginx 的location匹配是按最长前缀匹配/api/的优先级高于/。所以接口不会误被 fallback 到index.html这个顺序天然正确。但如果你把try_files写到了根location /里而接口请求恰好是以/开头的其他路径就需要特别当心确认接口代理的 location 先于根 location 匹配。3.4 history 模式下路由的“最后一道防线”404 页面有些团队做了这么一件事把所有匹配不到的路由统一指向一个 404 页面。这个页面本身是静态的不依赖前端路由。这样做的用意是什么就是要防止“未知 URL Nginx fallback 返回 index.html Vue Router 内部显示空白或兜底路由”这种场景。说到底SPA 的单页入口有一个固有矛盾任何未匹配的 URL 在服务端都会被 fallback 成同一个 HTML但前端 Router 不可能为所有的路径都建立路由表。所以 Vue Router 自己有一个 catch-all 路由一般长这样const router new VueRouter({ mode: history, routes: [ // ... 业务路由 { path: *, component: NotFoundComponent } ] })path: *是 Vue Router 3.x 的写法在 Vue Router 4.x 中变成了path: /:pathMatch(.*)*。这个 catch-all 路由会在用户访问一个不存在的路径时渲染一个 404 提示页。注意这只是一个前端的“伪 404”因为浏览器和服务器之间的通信状态码仍然是 200。如果确实需要返回真实的 404 状态码则要在服务器端配合处理。比如 Nginx 可以写location / { try_files $uri $uri/ fallback; } location fallback { rewrite ^ /index.html break; }这种写法还不能直接返回 404。要严格区分“合法路由路径”和“非法路径”单靠 Nginx 做不到因为 Nginx 不负责解析 JS 的路由表。真正可行的方案是让后端接口提供一个路由白名单或者干脆接受“前端伪 404” SEO 不强求的状态码语义。大多数内部系统、后台管理项目做到前端 404 页面就足够了只有面向公网且有 SEO 诉求的项目才需要抠这个细节。4. 两种模式的核心差异对比与选型建议写到这里两种模式的基本面貌应该已经清晰了。我把它们的核心差异整理成了一张表方便做决策的时候快速对照。4.1 核心对比维度对比维度hash 模式history 模式URL 形态带 #如 /#/user/123干净路径如 /user/123底层 APIlocation.hash hashchangehistory.pushState popstate浏览器兼容性所有现代浏览器均良好支持IE9 以下不支持现代浏览器无碍服务端配置无需特殊配置必须配置 fallback否则刷新 404搜索引擎 SEO对爬虫不友好相对友好第三方回调/分享可能被截断 / 不美观正常静态托管可用性任意托管控件均可用可能受限于 rewrite 能力开发调试地址栏带 #日志/调试略不方便地址栏干净便于观察这个表里有一项很多人可能没意识到hash模式不是不产生历史记录而是历史记录是堆在“hash 变化”这个层级上的history模式则把历史记录建立在真实 URL 上。这意味着实际使用中history模式在浏览器前进/后退的操作上更自然分享出去的 URL 也更“真实”。4.2 选型参考什么项目用哪种模式根据我的实际经验可以给出这样的选型建议后台管理系统、内网工具、低代码平台这类项目通常使用人数固定、无需 SEO、不对外分享 URL而且无法保证客户方的服务器一定开了 fallback 配置所以直接用hash模式减少沟通成本。除非团队对 URL 颜值有执念或者产品本身想做成对外服务的 SAAS否则没必要上history。内容型站点、官网、博客必须用history模式。SEO 前提下 hash 的劣势不用再强调。同时要有能力配置好 Nginx保证任意路径都 fallback 到入口文件。面向 C 端的 H5、小程序内嵌 H5优先history模式但如果遇到微信内嵌的特殊环境比如某些旧版 X5 内核有兼容问题或者某些应用的分享卡片取的是地址栏 URL 并且不希望带 hash那就得对具体场景做测试再定。纯静态托管OSS CDN、GitHub Pages如果托管商不支持 rewrite 配置就老老实实用hash模式如果支持自定义 404 页面且能接受 404 状态码可以用 history 作为次选方案。4.3 从 hash 切换到 history 的改动面如果项目最初是hash模式后期想切换成history模式改动其实不大但坑比较多。通常需要改这几个地方Vue Router 实例中mode: hash改为mode: history或 Vue Router 4 中createWebHashHistory()改为createWebHistory()。所有代码中的绝对路径问题。hash模式下资源路径相对请求入口都是/没问题但history模式下Vue Router 的路由交给了虚拟路径来管理如果项目部署在子路径下需要正确配置publicPath或base配置项否则 JS/CSS 加载不到。所有router-link和router.push中的目标路径不要带#。如果代码里混用了#/xxx这样的写法切换后必须清理掉。Nginx / 服务器的 fallback 配置必须在切换前准备好。检查所有全局跳转逻辑。比如登录失效后跳转登录页hash模式下可能写的是window.location.href /#/login这行代码在history模式下会变成直接跳服务器路径/login没有这个物理文件就会 404。这类散落在业务代码中的跳转往往是最大的隐患。这里有一个项目里真实遇到过的案例业务代码中有一个“分享给好友”的功能直接把window.location.href拼上路由发出去之前是 hash 模式所以正常切换成 history 模式之后没有清理掉这个拼接逻辑上线后分享出去的链接直接打不开了排查了半天才发现问题。5. 常见问题与排查技巧实录两类模式各自都有一些高频问题我把它们整理成排查清单按“问题现象 → 原因 → 解决方案”的方式写在下面方便大家遇到问题时直接对照。5.1 hash 模式相关问题问题现象可能原因解决方案路由跳转后页面内容不更新hashchange 事件未触发或监听时机过早确认 Vue Router 版本与初始化逻辑业务代码中不要手动修改 location.hash 又依赖自定义事件URL 中 hash 变成 %23代码中把 # 编码了检查是否有encodeURIComponent处理了路由地址URL 恢复为标准格式即可页面刷新后路由参数丢失路由参数放在了 query string 的 # 之前在hash模式下query 参数应该放在#之后形如/#/list?id123而不是/?id123#/list同一页面二次跳转失效路由地址没有变化使用router.push时传入的是相同路径Vue Router 默认会导航失败并报重复导航错误需带 timestamp 或 query 参数区分OAuth/支付回调地址异常第三方服务截断了 hash 部分尽量避免在回调 URL 中依赖 hash 路由改用 query 传参或 history 模式5.2 history 模式相关问题问题现象可能原因解决方案直接访问子路由刷新 404服务器未配置 SPA fallbackNginx 中加try_files $uri $uri/ /index.html;部署后 CSS/JS 加载失败 404资源路径为绝对路径/asset/...但应用部署在子路径下配置base或publicPath为子路径或用相对路径导入资源访问根路由正常但切换路由后点刷新又 404配置了 fallback 但只对根目录生效检查location匹配规则确认try_files写在范围覆盖所有路由的location /中接口请求返回 index.html 内容fallback 优先级高于接口代理调整 location 匹配顺序保证/api代理在/的 fallback 之前被匹配部署在 CDN 后面刷新出现 404CDN 回源到的是源站的 404 响应CDN 缓存了错误响应配置 CDN 的“错误码回源”策略或把 404 状态码的缓存时间设置为 0浏览器前进/后退后页面空白JS 报错或 popstate 没有被正确监听确认 Vue Router 版本查看控制台报错检查是否有其他 popstate 监听器干扰5.3 高频的“玄学”问题为什么我的 history 模式本地是好的上线就挂这类问题 90% 都是同一个原因本地 Dev Server 自带 fallbackNginx 没有配。也有一部分情况是配了 fallback 但配错了位置。我建议排查的时候按下面的顺序来先在浏览器打开线上地址按 F12 打开 Network 面板刷新页面看请求“文档”的响应状态码。如果是 404说明 fallback 根本没生效。看响应内容。如果是 Nginx 的默认 404 页面说明 fallback 没配置或没加载配置检查配置后要记得nginx -s reload。如果是应用最新的index.html的话就说明 fallback 已经生效问题可能在 JS 资源加载上。看 JS/CSS 资源请求的路径。如果资源请求的路径带了奇怪的公共前缀或者明显和部署路径不一致那需要检查publicPath的配置。看控制台是否有语法错误或者模块加载错误。有的部署会用 CDN 缓存可能导致新版本 JS 和旧版本 HTML 不匹配这种一般是缓存未过期导致的。上面这四步能解决我见过的 95% 以上 history 模式上线后白屏或 404 的问题。至于剩下的那 5%那就要看具体环境了比如服务商把.html后缀的请求单独做了处理或者 CDN 的缓存策略比较激进。5.4 几个你实际开发中一定会用到的调试技巧开发环境中如果你用的是history模式注意在vite.config.js或vue.config.js里开启historyApiFallback。Vite 默认已经开启但如果你的路由里出现了“路由器路径为中文或带空格”的情况要确保 fallback 能正确重写到index.html。用 Chrome DevTools 的 “Sensors” 模拟手机 UA 时有的场景会改变 hash 行为。这不算浏览器 bug但遇到排查问题困难时可以换一个环境试试。想在本地快速体验history模式的刷新 404 场景可以用vite preview预览构建产物Vite Preview 默认不开启 history fallback能帮你提前暴露问题。写代码时想要监听路由地址的变化来做埋点或者修改页面标题在 Vue Router 4 中可以用router.afterEach()在 Vue Router 3 中可以用afterEach全局守卫但注意这个钩子在hash模式和history模式下都能触发逻辑上统一处理即可。用 Vue Router 这么多年我个人的结论很简单能配服务器的项目优先history模式无法控制服务端配置的环境老老实实hash模式就好有一点不好看。实操中“切换模式”本身不是难事难的是把散落在业务代码里的 URL 假设全部揪出来那些写死了#/xxx的跳转、手动拼 URL 的逻辑往往才是切模式翻车的真正根源。我建议你在决定切换之前先全局搜索一下代码里所有的 location、href、path 相关逻辑心里有数再动手能省掉不少上线后打地鼠的功夫。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价