拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vue Router 的 history 与 hash 模式:原理、选型与部署避坑指南

做前端这么多年Vue Router的history模式和hash模式这个问题几乎每次面试都会被问到几乎每个项目也要踩一遍。我第一次做单页应用的时候项目跑起来 URL 是http://localhost:8080/#/home当时也没觉得哪里不对直到部署后同事说“你这个地址中间带个 #发给客户不太好看啊”我才认真去研究这俩模式的区别和背后的原理。今天用一篇文章把两种模式从原理、选型到部署配置、常见坑一次性聊透。不管你是刚接触 Vue 的新手还是被服务器 404 折磨过的老手这篇都应该能帮上忙。1. 路由地址是怎么“骗”过浏览器的1.1 hash 模式URL 里那段 # 不简单hash 模式的 URL 长这样http://example.com/#/user/list。关键就是那个##后面的部分叫做 hash 片段也叫 fragment。很多人以为它只是装饰其实它有一个非常关键的特性#及其后面的内容不会出现在 HTTP 请求里。也就是说你在浏览器里访问这个地址服务器实际收到的请求路径永远是http://example.com/后面的/user/list只在浏览器内部生效。这个特性就是 hash 模式不需要服务器配置的根源。浏览器原生提供了hashchange事件当#后面的内容变化时这个事件就会触发。Vue Router在 hash 模式下做的事就是监听hashchange事件拿到最新路径再根据路由表去渲染对应组件。整个过程不经过服务器纯本地完成。这里可以拿书签做类比你在一本书里夹了一枚书签书还是那本书但你告诉别人“我在第一百页夹了书签”。服务器看到的永远是“同一本书”至于你在哪一页那是你自己记的。hash 模式的本质就是这个——页面还是那个页面地址栏后面的路径纯粹是前端自己记的笔记。1.2 history 模式改地址但不发请求的魔法history 模式的 URL 长这样http://example.com/user/list看起来就是普通网站路径。它靠的是 HTML5 新增的 History API核心是pushState和replaceState两个方法。pushState可以在不刷新页面的情况下往浏览器历史记录里塞一条新记录同时改变地址栏的 URLreplaceState则是不新增记录直接替换当前记录。关键点在于这俩方法只改变地址栏不会触发浏览器向服务器发起任何请求。页面内容全靠前端 JS 根据路由表自己渲染。同时浏览器还提供了popstate事件用来监听用户的“前进”“后退”按钮操作。Vue Router在 history 模式下做的事情大概是点击router-link时用pushState改变地址然后渲染对应组件用户手动点浏览器后退按钮时监听popstate事件拿到旧的地址再渲染对应组件。这里容易混淆的一个点是pushState本身不会触发popstate事件。所以Vue Router必须在点击链接的时候自己完成“更新 URL 渲染组件”这两个动作而不是依赖事件回调。1.3 两种模式到底差在哪一次点击背后的完整链路从使用者角度最大的差别在两点URL 好不好看以及刷新页面时服务器能不能正确响应。从实现角度一次点击router-linkhash 模式去改地址栏#后面的内容并触发hashchangehistory 模式是调pushState修改地址栏并手动执行组件切换。刷新时的表现差别最大。hash 模式因为请求不会带 hash服务器永远返回根路径下的index.html所以刷新绝不会出问题。history 模式呢浏览器会向服务器发起对/user/list这个路径的真实请求如果你没有配置回退服务器找不到这个文件就会返回 404。用一个表格把核心差异列出来方便你随时翻维度hash 模式history 模式URL 形式http://xx.com/#/homehttp://xx.com/home服务器收到请求始终是根路径当前完整路径服务器配置不需要必须配置 URL 回退刷新页面稳定不会 404配置错误就会 404SEO 友好度较差相对较好浏览器兼容非常广依赖 HTML5 History API2. 选型思考什么场景该选哪个模式2.1 hash 模式真正吃香的地方虽然看起来 history 模式更“高级”但 hash 模式在真实项目中依然大量存在。最典型的是这几类场景内部管理系统、快速原型、演示项目。这种项目对 URL 有没有#无所谓最重要的是“零配置上线”。你随便找一个静态文件托管服务甚至用python -m http.server起一个本地服务hash 模式都能直接跑起来刷新也不会挂。另外老浏览器也是一个考量点。pushState虽然是很多年前的 API 了但 IE10 之前的老古董确实不支持。如果项目用户群体里有这类浏览器hash 模式反而更省心。当然现在这种场景越来越少了更多时候选 hash 是出于部署条件限制——比如公司规定前端包只能放到对象存储上不能动服务器配置那 history 模式根本没法用hash 模式成了唯一解。2.2 history 模式挂帅的场合对外项目基本都会选 history。理由很直接URL 干净用户在地址栏直接看到/product/123分享出去也更像正式网站。搜索引擎和社交平台抓取链接时带#的地址往往被认为不友好而标准路径至少是 SEO 工作的前提。如果你要做官网、落地页、面向客户的产品站history 模式几乎是必选。但 history 模式的隐含条件也在这里暴露出来它需要服务端配合。你不能把一个 history 模式的包扔到随便一个静态托管上就完事必须保证任何路径都回退到index.html。这意味着部署时你得能改 web 服务器配置。如果放到某些对象存储、CDN 服务上可能就不具备回退条件这就很尴尬。还有一点容易被忽略微信分享、第三方支付回调这类场景对 URL 里的特殊字符处理比较敏感带#的地址有时候会出问题。用 history 模式地址就是标准路径少很多麻烦。2.3 从零到一怎么挑怎么迁移我的选型逻辑很简单先回答三个问题能不能改服务器配置URL 观感重不重要要不要做 SEO三个答案都是“是”优先 history。如果服务器配置动不了或者项目是短期原生工具直接用 hash。还有一个容易被项目部踩的坑团队开发时习惯在本地起服务线上部署由运维统一管理结果开发用 hash 模式线上却配了 history 模式两类地址混在一起用户刷新就出问题。所以路由模式这件事必须在项目启动时就定下来写进技术方案里。真要从 hash 迁移到 history成本其实不高。核心是切换创建router时的方式路由表里定义的path可以完全一致组件代码也不用动。真正花时间的是部署配置以及全量回归测试。建议迁移时在测试环境把所有二级页面地址都手动刷新一遍别只在首页点来点去。3. 代码实操两种模式的配置与部署3.1 Vue Router 配置代码Vue 3 项目用的是 Vue Router 4配置方式是这样的import { createRouter, createWebHistory, createWebHashHistory } from vue-router // history 模式 const router createRouter({ history: createWebHistory(), routes, }) // hash 模式 const router createRouter({ history: createWebHashHistory(), routes, })Vue 2 项目用的是 Vue Router 3配置方式略有区别mode是一个字符串参数import VueRouter from vue-router // history 模式 const router new VueRouter({ mode: history, routes, }) // hash 模式 const router new VueRouter({ mode: hash, routes, })注意 Vue Router 4 把mode: history改成了history: createWebHistory()这种函数式写法刚开始容易搞混。但本质上还是一样的一个创建标准模式的路由一个创建 hash 模式的路由。切换模式时路由表完全不用动路由跳转的写法也不用动这点对迁移很友好。3.2 nginx 服务器配置解析history 模式上线时最常见的配置就是 nginx 的try_files。看一段最基础的配置server { listen 80; server_name example.com; root /var/www/myapp; index index.html; location / { try_files $uri $uri/ /index.html; } }try_files后面跟了三个参数逐个解释$uri表示按请求的路径去找对应文件比如访问/user/list就找/var/www/myapp/user/list这个文件$uri/表示如果文件不存在就找同名目录如果文件、目录都没有最后回退到/index.html也就是让前端路由接管。这样做之后用户直接访问/user/list时服务器返回的是index.html浏览器加载后前端 JS 再根据地址渲染出用户列表页面。这就是 history 模式部署的核心逻辑。但这里有一个容易被忽略的问题try_files把不存在的路径全部回退到了index.html如果 JS、CSS 文件路径配错了服务器也会返回index.html而不是 404导致页面白屏且报一堆 MIME 类型错误。所以更稳妥的做法是给静态资源目录单独加一个 locationlocation /assets/ { try_files $uri 404; }这样/assets/下的资源找得到就返回找不到就明确 404方便排查问题。除了 nginx其他服务器也有对应的配置方式。Apache 可以用.htaccess里的RewriteRule实现Node 服务可以用connect-history-api-fallback之类的中间件。核心思路都一样未知路径回退到index.html。3.3 部署在子路径时的配置如果你的应用不是挂在域名根路径而是挂在/admin这种子路径下就得多配一层。nginx 可以这样写location /admin/ { root /var/www; # 实际目录是 /var/www/admin try_files $uri $uri/ /admin/index.html; }这里try_files的回退目标写的是/admin/index.html因为当请求/admin/user/list时如果找不到对应文件回退到/admin/index.html才能再次进入这个 location并映射到正确的文件。同时前端的路由 base 也要配置。Vue Router 4 里是这样const router createRouter({ history: createWebHistory(/admin/), routes, })如果base不配路由默认从/开始地址栏会变成/user/list但应用实际挂在/admin/下面访问就完全对不上了。打包时的资源路径也要同步设置Vue CLI 项目在vue.config.js里配置module.exports { publicPath: process.env.NODE_ENV production ? /admin/ : /, }Vite 项目则在vite.config.js里配置export default defineConfig({ base: /admin/, })这里最容易出的问题就是路由 base 改了构建配置没改或者反过来结果要么资源 404要么路由对不上。每次部署子路径项目我都要把这两处对照检查一遍。3.4 开发环境与打包配置开发环境通常不用太操心。Webpack Dev Server 和 Vite Dev Server 默认都做了 SPA fallback所以你本地起服务时history 模式也是正常的直接访问/user/list没问题。但如果你要复现“部署到子路径”的场景开发服务器同样要配置对应的historyApiFallback或base否则本地跑得好好的一上生产就白屏。有一点要特别提醒如果你用的是 Vite默认appType是spa此时 dev server 才会做 SPA fallback。如果你为了某些原因把appType改成了别的值开发环境访问深层路由就会出现 404。这个配置项藏得比较深遇到开发环境刷新 404 时可以查一下。打包方面history 模式对静态资源的路径更敏感。如果代码里用了绝对路径引用图片、接口地址部署目录一变资源就可能全挂。建议项目里所有静态资源都走相对路径或统一通过构建工具的publicPath/base处理不要写死域名前缀。4. 高频踩坑与排查技巧4.1 history 模式刷新就 404 怎么办这个问题几乎是所有初用 history 模式的人都会遇到的。现象是本地开发一切正常打包部署后打开首页没问题一刷新/user/list就变成 404。原因很简单服务器不知道/user/list是什么。访问首页/时服务器能找到index.html访问/user/list时服务器去找文件列表发现没有这个文件就返回 404 了。解决方式就是上面说的——配置try_files让服务器把所有未知路径都指向index.html。如果已经配了try_files还是 404按这个顺序排查第一nginx 配置有没有重新加载改了配置要执行nginx -s reload才生效第二路径有没有带子目录root和alias是不是写错了第三前面还有没有其它层级的网关或 CDN 在拦截。实际项目里前端 nginx 配好了但因为前面还有一层网关直接返回 404 的情况我也见过不少。排查时用curl -I http://yourdomain.com/user/list看响应码如果返回 200 且 Content-Type 是text/html说明回退配置生效了如果返回 404那说明请求根本没到 nginx 的这条 location或者配置顺序有问题。4.2 hash 模式 URL 带 query 的兼容问题hash 模式也不是完全没有坑。有一个比较隐蔽的问题外部链接带 query 参数时query 应该放在#前面还是里面。Vue Router 的 hash 模式自身生成的地址是http://xx.com/#/path?queryxxxquery 是在 hash 内部的。但如果有外部链接写成http://xx.com/?sourceseo#/home也就是 query 在#前面Vue Router 就解析不到这个 query因为它在解析#后面的路径。这会导致路由守卫里拿不到route.query.source推广链接追踪时尤其蛋疼。解决方式是在应用初始化之前手动检查window.location.search把需要保留的参数合并到路由中。这个逻辑对两种模式都适用但 hash 模式下尤其容易被人忽略——因为很多人以为只要 URL 里有 query 就能在路由里拿到。还有一个坑是 HTML 锚点冲突。如果页面里有a href#section这种锚点链接点击后 hash 会变成#sectionVue Router 会以为路由变化了结果找不到匹配路由轻则警告重则白屏。处理方式是用原生scrollIntoView替代锚点或者通过scrollBehavior统一管理滚动位置。4.3 路由跳转异常与资源路径错乱还有一类问题是打包后资源路径错乱。典型现场是index.html能访问但里面引用的/js/app.js其实是找域名根目录下的文件服务器根本没有于是页面空白。这种情况最常见的原因就是部署在子路径但publicPath/base没设置。比如项目部署在https://example.com/admin/但打包时资源路径是/js/app.js浏览器加载资源时实际请求的是https://example.com/js/app.js自然找不到。解决办法是打包时把publicPath或base设置成实际部署路径nginx 的root或alias也要对应。如果是部署路径经常变动的项目可以考虑用相对路径但 SPA 的路由模式下相对路径容易和路由混在一起不是特别推荐。处理这类问题有一个经验白屏时先看浏览器的 Network 面板如果index.html正常但 JS 文件红色 404基本就是资源路径问题如果 JS 都加载了但页面空白再看 Console 有没有路由匹配报错或者组件加载失败。4.4 常见问题速查表问题可能原因解决方式history 模式刷新 404服务器没有配置 URL 回退配置try_files $uri $uri/ /index.html子路径部署白屏base/publicPath没设置同步修改路由 base 和构建配置hash 模式锚点失效#锚点与路由冲突使用scrollIntoView或scrollBehaviorhash 模式外部 query 丢失query 放在了#前面手动解析window.location.search并合并页面能打开但 JS 报 404静态资源路径错误检查publicPath与 nginx 的root/alias本地正常线上白屏构建配置与部署路径不一致对照检查构建配置和服务器配置5. 我的一点个人经验按我近两年的项目习惯默认都会用 history 模式前提是确认部署环境允许修改服务器配置。只要是能配try_files的环境我基本不会再退回 hash。但凡是客户临时要的 H5 活动页、放在对象存储上的静态项目、或者纯内网用的管理后台我会直接选 hash省心第一不折腾。还有一个习惯想分享不管用哪种模式路由配置里path的命名尽量保持稳定和有规律别今天用/user/list明天换成/list/user。因为模式的切换其实不像很多人想的那样伤筋动骨——路由表大体可以共用真正的成本在部署配置和回归测试。如果 path 本身乱改切换模式时会更痛苦。最后提醒一句上线前一定一定要在线上环境把每个路由都刷新一遍这个步骤省不得。很多 case 本地、测试环境都正常线上因为 CDN 缓存、多层网关之类的问题就是会随机给你冒出一个 404。刷新一遍把问题留在发布之前解决比上线后被用户发现要体面得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门