前端路由深剖:History模式与Hash模式的原理、配置与部署避坑指南
做前端开发的人尤其是用过 Vue Router 或者 React Router 的肯定绕不过这两个概念History 模式和 Hash 模式。我刚接触前端路由那会儿压根没在意这俩的区别反正npm run dev一把梭开发环境跑得好好的直到第一次把项目部署到服务器上一刷新页面直接报警告“404 Not Found”这才意识到路由模式这件事没搞清楚是要出大问题的。后来在好几个项目里反复踩坑、翻源码、查文档才把这两兄弟的脾气摸透。这篇就把 History 模式和 Hash 模式的原理、区别、配置方式以及我实际部署中遇到的那些坑一次性给你讲清楚。这内容适合谁看如果你正在用 Vue、React 这类 SPA 框架做项目打算从开发环境走到生产部署或者你已经在部署时遇到了“刷新就白屏/404”的问题再或者你只是好奇为什么有的网址带#、有的不带——那这篇就是写给你的。看完不说让你成为路由专家至少配置、排查、选型这些实操环节你能心里有数不用再到处搜零碎答案。1. 两种模式到底在做什么核心原理拆解想要真正理解 History 和 Hash 的区别光背结论没用得先把它们的工作机制弄明白。这俩模式本质上是解决同一个问题如何让一个单页应用SPA在不刷新页面的情况下切换页面内容同时还能让 URL 跟着变化并且支持浏览器的前进后退。1.1 Hash 模式URL 里的#到底是个什么东西Hash 模式的核心是 URL 中的#符号以及它后面的那一串内容。比如https://example.com/#/user/123里的#/user/123就是 hash 部分。这个#在 HTTP 协议里有个特殊身份它后面的内容是用来定位页面内某个位置的锚点anchor浏览器发起请求的时候压根不会把#后面的内容发送给服务器。也就是说你在地址栏里输入什么#之后的东西服务器看到的始终是https://example.com/。那前端路由是怎么利用这个特性的呢当用户点击页面里的链接或者执行路由跳转时路由库会修改window.location.hash比如从#/user/123改成#/order/456。这个改动会触发浏览器的hashchange事件。前端路由在初始化的时候会监听这个事件一旦检测到 hash 变了就解析最新的 hash 值再根据路由表找到对应的组件去渲染。这个过程有一个关键点修改 hash 不会导致浏览器向服务器发送新的 HTTP 请求也不会重新加载页面只是地址栏的 URL 变了然后前端代码自己做出响应。这就是为什么 Hash 模式部署到任何静态服务器上都能直接跑因为服务器自始至终只收到一个固定的路径请求根本不需要关心路由规则。我把 Hash 模式和实体书做个类比URL 里的#后面的内容就像书里的页码你可以随意翻到第 10 页、第 20 页但这始终是在同一本书的范围内操作不需要跑到图书馆前台去借一本新书。浏览器也一样它始终停留在同一个 HTML 文档上只是根据 hash 值的变化切换当前显示的内容块。1.2 History 模式去掉了#但要有服务端配合History 模式之所以能做到 URL 里没有#背后靠的是 HTML5 新增的 History API关键方法有三个pushState()、replaceState()和popstate事件。pushState()可以往浏览器的历史记录里添加一条新记录同时改变地址栏的 URL但这同样不会触发页面刷新也不会向服务器发请求。这是 SPA 路由能实现的基础前端代码可以自由地改变地址栏路径比如从/user/123变成/order/456然后前端自己根据路径渲染对应的组件。用户看到的效果是 URL 变了、页面内容也变了但浏览器没有重新向服务器要资源。问题出在哪如果用户在/user/123这个路径下点了刷新按钮或者直接把地址复制给朋友打开浏览器就会向服务器发起一个真实的 HTTP 请求请求的路径就是/user/123。服务器会想“我没这个文件啊”于是返回 404。这就是 History 模式部署上线后刷新白屏的根源。所以 History 模式的部署对服务器有一个硬性要求你需要配置一个 fallback 规则也叫回退规则把所有未匹配到真实文件的请求都返回到应用的入口 HTML 文件通常是 index.html。这样浏览器拿到 index.html 后前端路由再根据当前 URL 去渲染对应组件一切恢复正常。接刚才的类比History 模式相当于你把每个章节改成了一个独立的书名比如把“第一章”改叫“小明的冒险”把“第二章”改叫“小红的旅行”。但实体书还是那一本每次有人想看“小红的旅行”时你得提前和图书馆管理员说好不管访客报哪个书名你都给拿那本实体书。管理员没配合好访客就会空手而归对应到前端就是 404。1.3 两者本质区别就三句话总结一下两种模式的核心差别可以浓缩成三句话Hash 模式依靠 URL 中#后面的内容变化触发前端渲染浏览器不会额外发请求所以部署最简单。History 模式依靠 History API 修改地址栏路径刷新或直接访问时浏览器会向服务器发真实请求所以服务端必须配回退规则。Hash 模式多出一个#History 模式 URL 更干净但后者的“干净”是有代价的——必须在服务端做额外配置。2. History 和 Hash 的差异到底有多大多维度对比与选型判断前面讲了原理可能有人会觉得“看起来就是多个#的事嘛”。等到了具体项目里两者的差异会实实在在影响你的开发、部署、甚至 SEO。我从五个维度做了对比每一项都是实际项目中会遇到的问题。2.1 URL 形态一眼能看出的不同最直观的区别就是 URL 长什么样。Hash 模式是https://example.com/#/user/123History 模式是https://example.com/user/123。有人可能觉得“多个#而已没什么大不了”但在实际业务里这个差别很影响体验。比如你在浏览器地址栏输入一个 History 模式的 URL回车打开用户看到的路径和他的浏览位置是吻合的而 Hash 模式的 URL 复制给同事时#后面的内容在某些 IM 工具里可能会被截断或者被奇怪地处理那种带#的地址看起来也总有点“临时页面”的感觉。对追求 URL 整洁度的产品来说History 模式几乎是必选。2.2 服务端要求一个要求你配置一个完全不用管这是两者最本质的差异。Hash 模式因为#后面的内容永远不会发到服务器所以前端项目打包成静态文件后随便扔到 Nginx、Apache、OSS、GitHub Pages 上都能正常工作。哪怕你直接在本地双击 index.html 文件打开Hash 模式在一定程度上也能跑资源路径别配错的话。History 模式就不行了。所有静态资源服务器、后端应用服务器都必须配置“当请求路径找不到对应文件时返回 index.html”。这个配置不是可选项而是必选项。如果配置漏了就会出现“首页打开正常点进子页面一刷新就 404”的经典事故。2.3 SEO 能力History 模式完胜但不是万能药SPA 本身就 SEO 不友好因为页面内容全靠 JavaScript 运行时渲染搜索引擎爬虫如果只抓 HTML 源码看到的往往是一个空的div idapp。但相对而言History 模式下每个路由有独立的 URL爬虫至少能区分不同的页面链接Hash 模式由于#后面的内容不会提交给服务器早期的搜索引擎根本不会把 hash 后的路径当作独立页面收录。现在 Google 的爬虫能执行 JavaScript 了Bing 也能执行一些但百度的支持还是有限。所以如果你的应用非常依赖搜索引擎自然流量并且是用 Vue/React 这类方案做的通常建议优先选 History 模式保证每个页面有干净的 URL。配合 SSR服务端渲染或 SSG静态站点生成解决内容渲染问题Vue 生态的 Nuxt、React 生态的 Next.js 就是干这个的。但如果你是纯前端 SPA就算用了 History 模式SEO 问题也没法靠路由模式本身解决需要额外的预渲染prerendering或者 SSR 方案。2.4 用户体验与兼容性老浏览器是少数场景的分水岭从用户体验来说History 模式 URL 更自然、分享更友好。另外还有一个小细节Hash 模式如果服务端不支持自动刷新在某些场景下会有一点“跳跃感”因为每次路由切换是从#/a变成#/b浏览器可能会把 hash 变化记入历史记录产生不必要的浏览记录干扰用户的后退操作。History 模式用pushState能更精细地控制历史记录栈。兼容性方面Hash 模式几乎是全浏览器通吃包括远古的 IE6。History 模式要求浏览器支持 HTML5 History APIIE9 及以下是不支持的。不过 2024 年了除非你的用户群体还在大量使用老旧浏览器这个问题基本可以忽略。如果你实在要兼顾可以用vue-router之类库内置的能力在 History 模式不支持时自动 fallback 到 hash 模式。2.5 选型判断什么时候用哪个最合适根据我自己的项目经验选型时可以按这个思路来判断内部管理系统、后台管理系统、快速原型项目用户量小、不需要 SEO、部署环境简单选 Hash 模式省心省力。面向公众的内容型站点、商城、企业官网对 URL 美观有要求、可能需要 SEO选 History 模式同时一定要配置好服务端。团队对部署环境有完全控制权比如 Nginx 是自己维护的那无脑选 History 模式URL 干净是长期收益。部署在 GitHub Pages 这类只能放静态文件、没法改服务端规则的平台推荐 Hash 模式省得踩平台限制的坑。我还做过一个容易被人忽略的点如果你做的应用有一个“分享到微信/钉钉”的需求URL 里带#的链接在部分客户端里会丢失 hash 后面的内容。比如从https://x.com/#/detail/100分享出去对方打开后可能会变成https://x.com/路由直接回到首页。这种情况下History 模式就是刚需。这个坑让我一次就记住了后来凡是涉及分享功能的项目我第一反应就是确认路由模式。3. 手把手把两种模式配到能跑前端框架与开发环境配置原理和选型讲完了进入实操环节。不同框架、不同构建工具在配置上有细微差别我按最常见的几种组合来写。3.1 Vue RouterVue 2 和 Vue 3 的写法差异先看 Vue 3 Vue Router 4。用 Vite 创建的项目路由一般长这样import { createRouter, createWebHistory, createWebHashHistory } from vue-router import routes from ./routes // 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 const router new VueRouter({ mode: history, // history 对应 History 模式hash 是默认值可以省略 routes })这里有个细节值得注意Vue Router 3 默认就是 hash 模式mode: hash很多人根本没设置过就知道“路由能跑”就是这个原因。到了 Vue Router 4 改成了显式调用工厂函数其实也是想提醒开发者你选的模式是什么自己心里要有数。3.2 React RouterBrowserRouter 和 HashRouter 的选择React Router 的配置更直接不同 Router 组件代表不同模式。v6 版本里import { BrowserRouter, HashRouter, Routes, Route } from react-router-dom // History 模式用 BrowserRouter function App() { return ( BrowserRouter Routes Route path/ element{Home /} / Route path/user element{User /} / /Routes /BrowserRouter ) } // Hash 模式用 HashRouter // function App() { // return ( // HashRouter // Routes // Route path/ element{Home /} / // Route path/user element{User /} / // /Routes // /HashRouter // ) // }React Router 旧版本里还有一种Router history{browserHistory}的写法在 v4 之后就不推荐了现在直接用 BrowserRouter 即可。3.3 开发环境为什么 dev server 从来不出事你可能好奇过我在开发环境用npm run dev用 History 模式也没见刷新 404 啊为什么部署到服务器就崩因为现代前端开发服务器早就内置了回退支持。Vite 开发服务器内部就实现了 connect-history-api-fallback 类似的逻辑webpack-dev-server也有一个配置项叫historyApiFallback默认就是开启的。也就是说开发环境的服务器收到一个找不到的路径请求时自动给你返回 index.html 了你压根没感知到这个环节。但生产环境用的 Nginx 并不会替你默认做这件事。所以本地能跑不代表生产能跑这是最典型的“开发环境骗了你”的情景。如果你在 Vite 里用的是 history 模式又确实想关掉这个回退看效果可以改vite.config.tsimport { defineConfig } from vite export default defineConfig({ server: { fs: { strict: false } } })不过一般没人会关。真正需要调整的是应用部署在子路径下的情况比如https://example.com/my-app/。这时 Vite 需要配置baseexport default defineConfig({ base: /my-app/, // 部署在子路径时必须设置否则静态资源会 404 })Vue Router 也要对应配置createWebHistory(/my-app/)和 base 保持一致否则路由匹配会错乱。3.4 不同构建工具的配置要点速查为了方便对照我把常见构建工具在 History 模式下的开发配置整理成了一张表构建工具配置项配置值说明Viteserver.historyApiFallback默认开启需要关闭时可设 false但很少用Vitebase如/my-app/子路径部署时必设影响资源加载路径webpack-dev-serverdevServer.historyApiFallback: true默认开启支持对象语法可配置 rewrites 规则vue-cliVue 2devServer.historyApiFallback默认开启同 webpackNext.js无需配置内置支持框架自带服务端渲染和路由处理Nuxt无需配置内置支持同上这里想多说一句很多人只知道设置historyApiFallback: true但没意识到这个配置的本质是“把找不到的 GET 请求全部转给 index.html”。如果你在开发环境有自定义的 mock 接口路径被这个 fallback 吞掉了请求就会返回 index.html 的 HTML 内容而不是预期的 JSON接口报错提示也会很怪。这种情况可以在historyApiFallback里用rewrites配置白名单排除掉。3.5 从零跑通一个 History 模式的 Vue3 项目示例很多新手卡在“代码写好了怎么验证模式生效”这一步这里给一个从零起步的最小示例。第一步创建项目node 版本建议 16 以上npm create vuelatest my-app cd my-app npm install第二步打开src/router/index.js改成使用createWebHistoryimport { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: /, name: home, component: HomeView }, { path: /about, name: about, component: () import(../views/AboutView.vue) } ] }) export default router注意createWebHistory(import.meta.env.BASE_URL)这个写法很关键它会把 Vite 的base配置自动拼到路由前缀里避免子路径部署时出现路由错乱。第三步直接访问http://localhost:5173/about能正常显示 About 组件说明开发环境已经按 History 模式工作了。接下来就是验证生产环境。执行npm run build把dist目录丢给 Nginx然后在 Nginx 里配好回退规则规则见下一章。没有配的话你访问/about刷新一次就能亲眼看到那个折磨过无数人的 404 页面了。4. 上线部署的关键一步Nginx 与后端服务器的 History 模式配置这一章是整篇文章的重头戏。前面说过History 模式部署时服务端必须配置 fallback 规则否则刷新就 404。下面从最常见的前端部署方案开始逐个讲。4.1 Nginx 的 try_files 配置最经典的解法假设你的前端打包文件放在/var/www/my-appNginx 配置可以这样写server { listen 80; server_name example.com; root /var/www/my-app; index index.html; location / { try_files $uri $uri/ /index.html; } }核心就是location /块里的try_files $uri $uri/ /index.html;。这句话要拆开理解$uri先把请求的原始路径当文件找一下比如有人请求/logo.png如果root目录下确实有这个文件就直接返回不再往下走。$uri/如果当文件找不到就尝试当作目录处理看看有没有对应的索引文件由index指令决定。比如有人请求/about如果/var/www/my-app/about是个目录且有index.html就直接返回这个页面。/index.html前两步都失败了说明这个路径下没有真实文件那就统一返回应用入口页index.html让前端路由去解析。这个配置覆盖了三种情况静态资源文件存在就直接返回目录形式能匹配就用目录否则交给前端路由兜底。非常健壮。如果你把静态资源放在了 CDN 或者单独的域名下Nginx 配置要区分开。比如静态资源走static.example.com那location配置不要用根路径try_files而是专门划一个location /assets/的规则否则资源请求也会被 fallback 到 index.html导致资源加载失败。4.2 嵌套路径部署location 前缀和 try_files 的配合不是所有项目都部署在域名根路径更多时候是放在某个子路径下比如https://example.com/admin/。这时 server 块配置要改成server { listen 80; server_name example.com; location /admin/ { alias /var/www/my-app/; try_files $uri $uri/ /admin/index.html; } }这里最关键的变化有两个用了alias而不是roottry_files的最后一项变成了/admin/index.html。如果这两处配错了会出现两类典型问题一是资源路径请求变成了/admin/assets/xxx.js实际文件却在/usr/share/nginx/html/assets/xxx.js导致图片 CSS 全部 404二是访问/admin/user/1时 fallback 到了/index.html页面能开但路径不对路由匹配混乱。另外一个常见的坑是如果你在 Nginx 里配了location /的try_files但项目里还用到了后端 API 接口比如/api/user/list这个路径也会被 fallback 到 index.html。正确的做法是把 API 路径单独拎出来location /api { proxy_pass http://127.0.0.1:8080; } location / { try_files $uri $uri/ /index.html; }这条规则能把动态请求和静态资源分隔开避免接口请求被前端路由吞掉。4.3 不只是 Nginx其他服务器和后端语言怎么配除了 Nginx前端项目还可能部署在 Apache、Node.js 服务或者由后端框架直接托管。这里整理了几种常见场景的配置写法。Apache 需要通过.htaccess或者虚拟主机配置启用mod_rewriteIfModule mod_rewrite.c RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] /IfModule这段规则的意思和 Nginx 的try_files是相通的请求的文件确实存在!-f表示不是文件且不是目录!-d时才交给 index.html 处理。注意如果你的站点部署在/admin子路径RewriteBase要改成/admin/最后一行也要改成/admin/index.html。Node.jsExpress场景通常写在中间件里const express require(express) const path require(path) const app express() // 静态资源托管 app.use(express.static(path.join(__dirname, dist))) // SPA fallback所有非文件请求都返回 index.html app.get(*, (req, res) { res.sendFile(path.join(__dirname, dist, index.html)) }) app.listen(3000)注意app.get(*)要放在所有 API 路由定义之后否则/api/user/list也会被 SPA fallback 拦截导致接口返回 HTML 而不是 JSON。如果你用的是 Express 5通配符写法改成了app.get(/*splat)别记混了。Spring Boot 托管前端构建产物时需要实现 WebMvcConfigurer 并添加 view controllerConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:\\w}) .setViewName(forward:/index.html); registry.addViewController(/**/{spring:\\w}) .setViewName(forward:/index.html); registry.addViewController(/{spring:\\w}/**{spring:?!(\\.js|\\.css)$}) .setViewName(forward:/index.html); } }这套配置的核心思路是把所有不带静态资源后缀js、css 等的路径转发给 index.html同时绕开资源文件。如果你看到这里觉得麻烦也可以直接用 Spring 的forward转发方案或者把前端放到 Nginx后端只提供 API职责分离更清爽。4.4 配置前先想清楚回退规则这样写才不会出错我给try_files配置排个优先级方便新手理解真实文件请求图片、JS、CSS、字体优先级最高必须能直接命中。API 请求次之应该转发给后端服务不要被 fallback 吞掉。前端路由路径最后兜底全部交给 index.html。如果你发现某个部署场景下静态资源 404、接口 404、或者页面刷新 404按这个优先级逐层排查基本都能定位到是哪个环节的规则没写对。5. 我踩过的坑和排查思路典型问题实录这一章全部来自我实际遇到的线上问题每一个都对应一条血泪教训。我按“现象—原因—解法”的形式整理后面还有一个速查表方便你遇到问题时快速对照。5.1 刷新后 404最经典也最容易中招现象项目用 History 模式部署到 Nginx首页/能正常打开点导航跳到/user/123也正常但只要按一下 F5 刷新页面就变白浏览器显示 404。原因刷新时浏览器向 Nginx 请求/user/123这个路径Nginx 去root目录找发现没有名为user的文件也没有123目录于是直接返回 404压根没走到前端路由那里。解法给location /加上try_files $uri $uri/ /index.html;。我见过太多人问这个问题九成都是 Nginx 没做回退配置。这个解法生效后Nginx 遇到/user/123找不到文件时就会把index.html返回给浏览器前端路由拿到 index.html 后再根据当前 URL 渲染对应的用户页面一切恢复正常。排查时怎么快速确定是这个问题用 curl 试一下curl -I https://example.com/user/123如果返回的是404 Not Found基本就是 Nginx 回退规则没配好如果返回200 OK说明 Nginx 已经正确回退到 index.html 了问题可能出在前端路由配置上。5.2 页面能打开但静态资源全部 404现象配置好try_files后刷新页面不再 404 了但页面样式全丢了控制台里一堆Failed to load resource: the server responded with a status of 404全是.js、.css文件的请求。原因这是资源路径问题。一般是base或publicPath配置缺失导致的。比如项目部署在https://example.com/admin/下但 Vite 的base没设置默认是/打包后的资源引用路径就是/assets/index.js浏览器会在https://example.com/assets/index.js找资源当然找不到因为真实路径是/admin/assets/index.js。解法部署在子路径时把 Vite 的base和 Vue Router 的createWebHistory参数都设置为/admin/// vite.config.ts export default defineConfig({ base: /admin/ })// router/index.js const router createRouter({ history: createWebHistory(/admin/), routes })如果是 Webpack 构建对应改output.publicPath的值。这个坑的隐蔽性在于本地开发环境一切正常因为 dev server 就是根路径一旦放到子路径部署资源路径就全乱了而且报错不直观需要看 Network 面板才能定位。5.3 Hash 模式的锚点冲突问题现象某个内部项目一直用 Hash 模式页面里有一个“回到顶部”的链接a href#top点击后发现没有跳到页面顶部反而把路由从/#/list切到了/#/top页面直接变成了空白或者跳到了错误路由。原因Hash 模式的#就是路由的核心页面内的锚点定位功能天然会被路由“抢走”。浏览器看到#top时不管你是想让页面滚动到 id 为 top 的元素还是想切换路由它都只触发一个hashchange事件而前端路由接管了这个事件后就会尝试匹配top这个路由。解法要么避免在 Hash 模式项目里使用传统的#锚点改用 JS 的scrollIntoView()要么给锚点名称加个前缀比如#anchor-top然后在路由拦截器里排除这类 hash但这实现起来极容易出 bug。最省心的方案还是上文提过的思路内部系统老老实实用 Hash 模式但页面内别再用#锚点了。5.4 History 模式下微信内打开分享链接丢失路径现象项目用 History 模式部署在微信里打开https://example.com/user/123页面能正常展示但把链接分享给朋友后对方打开却回到了首页。原因微信内置浏览器的分享逻辑在某些版本里会丢失 URL 路径层级或者把 URL 截断成https://example.com/。这个行为在不同版本、不同手机上不一致很不稳定。解法这个没有一劳永逸的代码方案。我当时的做法是在分享出去的链接后面加一个参数作为兜底比如https://example.com/user/123?fromshare然后在首页读取这个参数判断是否需要重定向到具体页面。虽然麻烦但至少分享场景下能把用户带到正确位置。如果你要做社交分享类应用路由模式的选择要和这个因素一起评估。5.5 环境正常、部署异常dev server 和生产环境的配置差异现象本地npm run dev怎么刷新都对放到服务器上就是各种 404。原因我在第三章提过开发服务器内置了 fallback 机制生产环境的 Nginx、Apache 默认没有。这大概是 History 模式新手最容易踩的坑因为它形成了“代码肯定没问题”的错觉。解法建议把这一条当成固定流程来做项目第一次部署时先把生产环境的回退规则配好再验证刷新行为。别指望开发环境的行为可以代表生产环境。我在部署清单里加了一行“route fallback configured”每回走完清单再上线就没再出过这类问题。5.6 常见问题速查表现象可能原因检查方法解决办法刷新页面 404Nginx 未配置 try_filescurl -I 访问子路径在 location / 加 try_files 配置页面能打开但 CSS/JS 404base/publicPath 配置错误打开 Network 面板看资源请求路径设置正确的 base 和 route base接口请求返回 HTMLAPI 路径被 fallback 覆盖看响应头 Content-Type将 /api 路径单独配 location部署在子路径路由错乱base 与路由 base 不一致检查 router 配置和 index.html 资源路径统一设置 base 和 createWebHistory 参数点锚点却切换了路由Hash 模式与 # 锚点冲突检查链接 href用 scrollIntoView 替代 # 锚点清除缓存后 404服务端缓存了旧页面或配置未生效查看响应头、重启 Nginxnginx -s reload确认缓存策略localStorage 存了根路径的数据子路径取不到浏览器对 localStorage 按 origin 隔离检查存储 key明确存储策略必要时用统一 key 前缀里面有一个细节值得展开说一下localStorage 那条。很多用 Hash 模式的项目从/#/a跳到/#/b浏览器始终停留在同一个 originlocalStorage 能正常共享。但如果项目从 Hash 模式换到 History 模式或者从根路径部署改成子路径部署localStorage 的访问范围可能发生变化原本写在代码里的“直接取 localStorage 里某个 key”的逻辑就会拿到 null间接引发业务报错。这类问题不太容易和路由模式关联起来但确实见过不止一次。6. 我的一点实战心得路由模式这件事值得提前规划写了这么多最后聊几句实在的。路由模式这个事看起来就是个配置项但它在项目里的影响是“扩散型”的选型会影响 URL 结构URL 结构会影响分享和 SEO服务端配置影响部署方式部署方式又决定维护复杂度。如果项目上线了才想起要换模式代价可不小所有 URL 变收藏夹失效重定向规则要写搜索引擎收录要重新做。所以我的建议是项目一开始就花五分钟想清楚这三件事部署在什么环境服务器规则能不能改是否需要 SEOURL 里能不能容忍#的存在。想清楚了后面能省一大块返工的时间。另外如果你正在维护一个老项目想从 Hash 模式切到 History 模式一定记得给旧 URL 写重定向规则比如把/#/user/123301 重定向到/user/123否则旧链接全失效外部流量损失得悄无声息。我当时给一个工具站做切换的时候在 Nginx 里加了一条规则处理这类带#的旧地址花了一个下午才把所有边界情况想全但后续基本没收到过用户反馈说链接打不开。最后分享一个小技巧如果你临时想快速确认当前项目是哪种路由模式直接看地址栏就行。有#是 Hash没#是 History简单直接完全不需要翻代码。但如果两种模式混着用只在某个子路由里用了 hash 做兼容那就只能以路由配置为准了。