想象你正站在一座巨大城市综合体的入口,手里攥着一张地址写着“3F-美食广场-川菜馆”。这座综合体名叫“SPA中心”,它把过去散落在街区的所有小店铺都搬进了同一栋楼。过去,每访问一家店都要走出旧楼、再进新楼——这是传统多页应用;现在,所有店铺在同一栋楼里,你要做的只是根据地址在不同楼层和展厅间穿梭,而帮你认路、带路、决定展示哪个店铺的“内部导航系统”,就是前端路由。它能让你在网址栏输入地址时,不刷新整个大楼,就能迅速把对应“店铺”呈现在你面前。这套导航系统有很多精细设计:地址用什么格式(Hash还是History)、导航手册怎么写(React Router还是Vue Router)、哪些区域需要安检(路由守卫)、哪些店铺可以到门口才装修(懒加载),以及回到上次的楼层时恢复位置(滚动行为)。本文会用这座“路由之城”的运营故事,把前端路由管理讲透。
掌握前端路由管理,是搭建现代Web应用(如中后台系统、电商SPA)的必备能力。熟练使用React Router和Vue Router、理解底层并做性能优化,能让你在团队中独立负责应用导航与权限架构,这也是前端进阶面试的高频考点。
可视化演示
展示Hash路由与History路由两种模式下URL变化触发路由匹配和组件渲染的过程,切换模式对比差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Hash路由模式 URL中#后路径变化触发hashchange事件 路由器匹配路径渲染对应组件 URL#path → hashchange → render
路由基础概念
在这座“SPA综合体”里,每个页面不再是街边独立的小屋,而是楼内的一个个展厅。传统多页应用(MPA)好比用户每访问一个“房间”都要走出整栋建筑再走进另一栋——浏览器会重新请求HTML并整页刷新。而单页应用(SPA)把所有的“展厅”都集中在一栋楼内,用户只需在楼内移动,不用出门。前端路由,就是为这栋楼设计的“内部导览系统”:它根据你当前手上的地址(URL),决定应该带你进入哪个展厅,并把这个展厅的内容呈现到眼前。
路由的核心是“映射”二字:URL的路径部分是一把钥匙,组件是锁。路由器维护着一张巨大的“钥匙-展厅”对照表。当URL变化时,路由器的查表函数会迅速找出对应的组件并渲染到页面容器中。这个过程不需要向服务器再要一份新的HTML,所有切换都在浏览器内完成。为了让读者理解这个查表逻辑,我们可以形式化地写出:
理解这张“钥匙-展厅对照表”,你就抓住了路由的命脉。无论后面讲Hash还是History、React还是Vue,本质上都是在回答两个问题:地址怎么变?变完之后查哪套表?导航系统的框架或许不同,但服务的核心目标一致——让用户在大楼里快速、准确地抵达想去的展厅。
/home)映射到具体的组件(如首页组件)。- 路径输入用户输入、点击链接或调用跳转方法,产生了URL中的路径部分,如
/home。 - 路由器查表路由器在路由表中寻找与该路径匹配的映射规则,路由表是一组“路径→组件”的键值对。
- 组件输出找到匹配项后,把对应的组件渲染到页面指定的容器里,完成一次视图切换。
Hash路由原理
最初的地址格式设计,采用了“门牌号后加备注”的技巧。URL中 # 号后面的部分被称为Hash(片段标识符),形如 https://site.com/#/home。这个“备注”很有意思:浏览器不会因为 # 后面的内容变化而向服务器发起新的网络请求,它只会在内部记录这个片段的变化,并触发 hashchange 事件。也就是说,整栋大楼的门牌地址没有变,变的仅仅是你在楼内导览图上用笔圈出的“当前位置”。
Hash路由的前端实现,就是监听这个 hashchange 事件。当用户点击一个 href="#/about" 的链接时,URL的hash变成 #/about,事件触发,前端拿到 about 字符串去路由表中匹配,然后渲染对应组件。因为整个过程不涉及网络请求,所以Hash路由部署简单,任意静态服务器都能支持,不需要额外的服务端配置。但缺点也很明显:URL中多了一个 #,既不美观,也不利于搜索引擎抓取(许多爬虫会忽略hash后面的内容)。
Hash路由就像大楼里“楼层标注卡”的折角标记:你换标记时不需要惊动物业,也不会让整栋楼重新登记。它的简单可靠让它成为小型应用和内部系统的首选。不过,当你希望前台门面看起来正式、简洁,且愿意花精力维护物业配置时,就该请出下一位选手——History路由。
https://site.com/index.html#/user/123 的 hash 是 #/user/123。- 定位井号浏览器解析URL时,遇到
#就把后面的内容单独提取出来作为hash。 - 截取字符串路由库拿到hash后去掉开头的
#,得到路径字符串,如/user/123。 - 监听变化当hash变化时,
window触发hashchange事件,回调函数中重新查表渲染。
History路由原理
如果说Hash路由是在门牌号后贴便签,那么History路由就是直接“改正式门牌”。它借助HTML5提供的History API(pushState、replaceState、popstate事件),让URL看起来与传统的多页地址完全一样,例如 https://site.com/user/123。前端调用 pushState 改变URL,浏览器不会自动请求新页面,但会触发 popstate(在用户点击前进/后退时)。前端监听该事件,获取当前路径并渲染对应组件。
这种“改门牌”的方式有一个重要的前提:当用户直接访问 /user/123 或刷新页面时,服务器必须把该路径的请求重定向到SPA的入口HTML(通常是 index.html)。因为真实的服务器上并没有 /user/123 这个物理文件,如果服务器不配合,就会返回404。常见的配置是在Nginx中添加 try_files $uri /index.html;,或在使用Node/Express时通过 app.get('*', ...) 返回入口文件。所以History路由“好看”,但“售后麻烦”。
History路由让地址栏变得干净整洁,也给SEO优化留出了空间(配合服务端渲染或预渲染)。但请记住,它是一把双刃剑:想拥有漂亮门牌,就必须让物业(服务器)做好重定向配置。现实中很多刚接触SPA的同学做上线部署时,就是因为少了这一步,导致刷新后一片空白。
- 修改地址使用
history.pushState(state, title, path)在不刷新页面的情况下修改地址栏。 - 监听popstate当用户点击浏览器的前进/后退时,触发
popstate事件,前端拿到新的路径。 - 服务端兜底直接访问深层路径时,服务器将所有非资源请求重定向到
index.html。
React Router配置
在“SPA综合体”中,React Router就是官方发放的“导览手册模板”。它用声明式语法告诉导航员:看到哪个路径,就带用户去哪个展厅。基本的配置骨架很简单:用 <BrowserRouter> 包裹应用,再用 <Routes> 和 <Route> 声明路径与组件的对应关系。
嵌套路由对应着综合体里的“展厅套间”:比如用户中心是一个大展厅,里面还有“订单”和“设置”两个小隔间。父级路由组件通过 <Outlet /> 占位,子路由在父级内部渲染。路径参数用冒号声明,如 /user/:id,在组件中通过 useParams() 读取;查询字符串则用 useSearchParams() 解析。React Router v6还提供了 createBrowserRouter 和 RouterProvider,让数据加载、错误处理等能力更加集中。
React Router的配置哲学是“UI与URL同步”。你把手册写好,剩下的查表、匹配、渲染都由框架代劳。注意在v6中,<Route> 必须直接或间接放在 <Routes> 内,路径匹配默认是前缀匹配,用 index 属性表示默认子路由。理解这套语法,你就能在项目里灵活搭建出多层级、带参数的复杂导航结构。
/user/123 时,动态片段 :id 捕获 123,作为参数传给组件。- 声明路由
<Route path="/user/:id" element={<User />} />告诉路由器“遇到此模式就渲染 User”。 - 匹配动态参数路由器将URL中的实际值与
:id对应,生成参数对象{id: "123"}。 - 嵌套渲染父路由的
<Outlet />位置渲染子路由组件,形成层级导航。
<BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/user/:id" element={<User />} /> </Routes></BrowserRouter>
Vue Router用法
另一栋“分店大楼”用的是Vue Router。它的核心思路与React Router类似,但实现更“显式”:你需要先创建一个路由实例,再告诉Vue应用“用这个实例”。创建方式如下:
Vue Router的动态路由匹配同样用 :参数名 语法,组件内通过 route.params.id 获取。与React不同,Vue Router内置了完整的导航守卫体系(beforeEach、beforeResolve、afterEach),这为权限控制提供了“官方安检通道”。在组件内还可以使用 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave 等路由守卫钩子,其中 beforeRouteUpdate 尤其适用于动态路由参数变化时重新拉取数据的场景。
Vue Router的声明式导航用 <router-link>,编程式导航用 router.push()。它的守卫机制非常适合做“进入区域前的证件检查”。当你把 beforeEach 与登录状态结合,就能构建一套完整的访问控制流程,这正是下一节的内容。
routes 数组就是一张路由表,每个记录对象包含 path、component 等字段。- 创建实例调用
createRouter({ history, routes }),传入路由表和历史模式。 - 动态匹配
/user/:id与当前路径比较,捕获参数。 - 挂载应用
app.use(router)使整个应用获得$router和$route。
import { createRouter, createWebHistory } from 'vue-router';const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: Home }, { path: '/user/:id', component: User, props: true } ]});
路由守卫权限
综合体里总有一些VIP区域,不是谁都能进。路由守卫就是站在区域入口的“安检员”:他会检查你有没有通行证(登录Token),你的级别够不够(角色权限),如果不符合要求,就把你引导到登录处或权限提示页。
Vue Router提供全局前置守卫 router.beforeEach((to, from, next) => { ... }),在每次导航确认前执行。一个典型的登录认证流程:判断 to.meta.requiresAuth 为真且没有Token,则 next('/login') 并携带 redirect 参数;如果已登录访问登录页,则跳回首页。React Router没有内置守卫,但可以通过高阶组件(HOC)或包装 <Route> 组件实现同样的效果,比如:
权限控制的本质是“前端体验层的过滤”,而非安全边界。真正的数据安全必须依赖后端接口鉴权,前端守卫只是为了给用户提供合理的跳转反馈。
守卫是导航系统中最有“人情味”的一环:它知道哪些区域对你开放,哪些还差一个会员等级。但请记住,安检员只是引导员,不是执法者;真正的机密数据保护,要靠后端的保险柜(权限校验)来保证。
next() 放行,还是调用 next('/login') 重定向。- 读取Token从
localStorage或状态管理库中获取当前用户的登录凭证。 - 判断权限检查目标路由的
meta是否要求登录或特定角色。 - 决定放行满足条件调用
next(),否则调用next('/login')。
function RequireAuth({ children }) { const token = useAuth(); return token ? children : <Navigate to="/login" />;}
懒加载优化
试想,如果把综合体所有展厅的装修材料都堆在入口大厅,那顾客进门光看装修就得等半天。聪明的做法是:入口只放一张简洁的前台(核心框架),每个展厅的家具建材(组件代码)等顾客真正推门时才快速送入。这就是前端路由的懒加载。
具体实现上,React使用 React.lazy(() => import('./Home')),配合 <Suspense fallback={<Loading />}> 包裹路由组件;Vue Router则直接用动态导入语法 () => import('./Home.vue') 作为组件值。打包工具(Webpack/Vite)会根据这些动态导入自动把代码拆分成多个chunk,路由跳转到哪个页面,就下载哪个chunk。首屏只需要加载主框架和当前页的代码,体积大幅下降,加载速度显著提升。
懒加载并不是越多越好——如果每个页面都极小,拆分成大量几十KB的chunk会造成请求碎片化。一般建议按“页面级”划分:整个页面一个chunk,而不是把页面里的按钮也拆出去。把握好粒度,你的导航系统才能又快又稳。
- 动态导入使用
import()语法让打包器将组件拆分为独立chunk。 - Suspense兜底React 在异步组件加载完成前显示 fallback 内容。
- 路由触发加载只有访问对应路由时才执行
import()并渲染组件。
滚动行为处理
在综合体里逛展厅,用户可能在“商品列表”页面滚到很下面的位置,然后点进详情。当他按返回键回到列表页时,如果系统把他带回顶部,他就要重新滚动,体验极差。好的导航系统应该记住他之前的“观看高度”,回来时自动恢复。
浏览器原生刷新页面时会恢复滚动位置,但SPA的“无刷新跳转”不会自动这么做。React Router v6.4+ 提供了 <ScrollRestoration /> 组件(基于数据路由),可以在 location 变化时恢复滚动;Vue Router则更直接,在创建路由时提供 scrollBehavior(to, from, savedPosition) 函数,它返回一个位置对象:
还可以返回 { el: '#main', top: 100 } 滚动到指定元素。除了滚动恢复,路由的“404处理”也属于细节工程:React Router中用 <Route path="*" element={<NotFound />} />,Vue Router用 { path: '/:pathMatch(.*)*', component: NotFound } 捕获未匹配路径。
滚动行为是“细节魔鬼”的典型代表。一个体贴的导航系统,会在用户回头时递上他刚才放下的书签。404页面则是那个和蔼的“问询台”,告诉迷路的游客:你找的展厅不存在,但我们这里有地图。处理好这些边角,你的路由管理才算真正完整。
- 定义回调在Vue Router中传入
scrollBehavior函数。 - 判断来源若
savedPosition存在(前进/后退)则恢复原位置。 - 默认置顶普通导航返回
{ top: 0 },保证新页面从顶部开始。
scrollBehavior(to, from, savedPosition) { if (savedPosition) return savedPosition; // 前进/后退恢复 return { top: 0 }; // 新导航回顶部}
AI 应用场景
中后台管理系统的路由权限控制
阶段:前端工程化与权限架构设计
解决的问题:传统后台系统常用多套页面或刷新跳转实现权限判断,代码重复且体验割裂。AI赋能的现代中后台用前端路由守卫统一管理访问入口,根据用户角色动态过滤菜单和可访问路由,未授权访问自动跳转登录页,让权限逻辑集中可维护。
具体案例:一家B2B公司的管理后台包含“数据看板”“订单管理”“用户管理”三个模块。实施时在路由表中为每个模块添加 meta.roles 字段,beforeEach 中读取用户角色与 meta.roles 比对,无权限则重定向到403页。同时根据角色动态生成侧边栏菜单,不同登录者看到的功能入口不同。
电商SPA的History路由与SEO改造
阶段:Web性能优化与搜索引擎优化
解决的问题:电商站使用History路由让商品详情页URL干净清晰(如 /product/10086),但直接访问该URL会因缺少服务端配置返回404。通过Nginx配置 try_files 与预渲染/SSR技术结合,既保留SPA交互体验,又让爬虫能抓取页面内容。
具体案例:某零食电商App采用Vue 3 + History路由,部署时在Nginx添加 try_files $uri /index.html;,并针对热门商品页做SSR预渲染。改版后,商品搜索点击率提升12%,首屏加载时间降低30%。
内容社区列表页的滚动恢复优化
阶段:用户体验优化
解决的问题:用户在长列表页下滑很久后点击文章,返回时列表位置被重置到顶部,导致反复滚动。利用Vue Router的 scrollBehavior 保存和恢复滚动位置,并配合 keep-alive 缓存列表状态,显著提升阅读连续性。
具体案例:一个技术博客社区在文章详情页增加“返回列表”按钮后,用户返回时列表位置精确复原。发布后,用户平均阅读深度提升18%,跳出率下降5%。
Python 演示
import re class RouteManager: def __init__(self): self.routes = [] # (pattern, component) def register(self, pattern, component): self.routes.append((pattern, component)) def match(self, path): for pattern, component in self.routes: # Convert :param to regex group regex = re.sub(r':(\w+)', r'([^/]+)', pattern) m = re.fullmatch(regex, path) if m: params = dict(zip(re.findall(r':(\w+)', pattern), m.groups())) return component, params return None, {} def navigate(self, path): component, params = self.match(path) print(f'[Router] {path} -> {component}, params={params}') if component: component.render(params) else: print('404 Not Found') class HomePage: @staticmethod def render(params): print('渲染首页') class UserPage: @staticmethod def render(params): print(f'渲染用户页 id={params.get("id")}') if __name__ == '__main__': router = RouteManager() router.register('/', HomePage) router.register('/user/:id', UserPage) router.navigate('/') router.navigate('/user/42') router.navigate('/about')
self.routes = []存放“路径模式→组件”的注册表,相当于前端路由表。regex = re.sub(r':(\w+)', r'([^/]+)', pattern)把/user/:id变成正则/user/([^/]+),用于匹配真实路径。re.fullmatch(regex, path)要求路径完全匹配,避免/user/123/abc误匹配/user/:id。params = dict(zip(re.findall(...), m.groups()))把正则捕获的多个值依次赋给参数名,得到{"id": "42"}。router.register('/', HomePage)注册根路由,等价于前端<Route path="/" element={<Home/>} />。router.navigate('/user/42')模拟用户访问,输出匹配结果。
常见误区
误区我觉得History路由的URL更好看,那就直接换掉Hash路由,应该不会有太大问题吧?反正都是前端做的事。
正解History路由依赖服务端将所有路径请求重定向到 index.html。如果你们的静态服务器没有配置 try_files,用户直接访问 /user/123 或刷新页面,就会得到404。这相当于你改了门牌号却没有通知物业,客人按新地址上门却发现根本找不到这栋楼。Hash路由则永远不会有这个问题,因为 # 后面的内容不会发给服务器。
为何常见:开发时用 npm run dev 的本地服务器已经默认支持History回退,导致很多同学只在部署生产环境时才暴露问题,于是误以为两者等价。
误区我在路由守卫里判断了用户角色,没有管理员权限就跳转到403页,这样数据就安全了吧?
正解前端路由守卫只影响界面展示和跳转体验,无法阻止恶意用户直接调用后端API。真正的数据防护必须在后端每个接口做鉴权。守卫就像一个前台接待员,她能拦住陌生人进入会议室,但机密资料锁在保险柜里,那才是后端的责任。
为何常见:很多初学者看到页面被拦截,就以为攻击者也无路可走,忽略了网络请求可以直接构造这一事实。
误区为了追求极致性能,我把每个组件甚至每个按钮都写成动态导入,这样首屏加载一定更快。
正解过度拆分会让页面变成几十个几十KB的小文件,浏览器需要发出大量HTTP请求,反而拖慢加载。懒加载的粒度通常是“页面级”或“功能模块级”,而不是“组件级”。就像综合体按“展厅”分批开放,而不是连一把椅子也等顾客来了才搬。
为何常见:性能优化焦虑让新手一味追求“按需”,却忽略了网络请求数量和打包缓存之间的平衡。
误区Vue Router有 `beforeEach` 很方便,React Router没有官方守卫,是不是只能放弃权限控制?
正解React Router v6虽然不提供内置守卫,但可以通过封装 RequireAuth 高阶组件或编写通用组件包裹 <Route> 来实现,甚至能在 createBrowserRouter 的 loader 中做重定向。比如:
为何常见:初学者容易被框架的“有/无”清单限制思维,实际上路由守卫的本质是“导航前的拦截逻辑”,这在任何路由库中都能用组合或包装实现。
误区我在 `/user/123` 页面,然后通过 `router.push('/user/456')` 跳转,以为组件会销毁重建,直接在 `created` 里请求就完事了。
正解Vue和React在路径参数变化时默认复用同一个组件实例(因为组件类型没变),created 不会再次触发。你需要在Vue中用 beforeRouteUpdate 或 watch,在React中用 useEffect 监听 params.id 变化后再发新请求。这就像旅行团到了同一个景点但换了不同的展馆,导游需要重新讲解,而不是把游客赶出大楼重新入园。
为何常见:很多教程只演示了从列表页进入详情页的首次加载,没提在详情页之间切换的场景,导致实际开发时遇到“URL变了但内容没变”的困惑。
面试题
Q1. 关于前端路由,下列说法正确的是?
- A.前端路由会导致页面整页刷新
- B.Hash路由刷新时不会向服务器发请求← 正确答案
- C.History路由无需服务器配置
- D.前端路由只能在Vue中使用
答案:B。Hash路由的 # 后内容不会参与HTTP请求,因此刷新时浏览器不会把hash传给服务器,自然也不会重新请求HTML,所以B正确。A错误,前端路由正是为了避免整页刷新。C错误,History路由需要服务端重定向配合。D错误,React Router同样常用。
Q2. 在React Router v6中,嵌套路由的子组件应放置在父组件的哪个位置?
- A.
<Outlet />标签← 正确答案 - B.
<Link />标签 - C.
<Routes />标签 - D.
<NavLink />标签
答案:A。<Outlet /> 是父路由组件中用于渲染子路由的“插座”。<Link> 用于声明式跳转,<Routes> 用于定义路由配置的顶层容器,<NavLink> 是带激活样式的链接。把子路由渲染在 <Outlet> 处,就像综合体的子展厅嵌在父展厅的指定位置。
Q3. 关于History路由,下列描述正确的是?
- A.依赖
hashchange事件完成视图切换 - B.直接刷新任意子路径都会返回入口HTML
- C.修改URL必须通过
location.reload() - D.使用
pushState修改URL不会触发页面刷新← 正确答案
答案:D。History路由使用pushState修改URL,浏览器不会因此刷新页面,D正确。它依赖popstate事件而非hashchange,A错。能否返回入口HTML取决于服务端是否配置重定向,不是History API自带的,B错。pushState不需要调用reload,C错。
Q4. Vue Router选项式API中,响应动态路由参数变化的组件内守卫是?
- A.
beforeRouteUpdate← 正确答案 - B.
beforeRouteEnter - C.
beforeRouteLeave - D.
onBeforeRouteUpdate
答案:A。beforeRouteUpdate 在同一个组件内参数变化时触发,适合重新拉取数据。beforeRouteEnter 在进入路由前触发,此时不能访问this;beforeRouteLeave 在离开前触发;onBeforeRouteUpdate 是Vue Router 4组合式API的对应写法,在选项式API中不能用。注意题干限定“选项式API”。
Q5. 路由懒加载的主要目的是?
- A.减少首屏加载时间← 正确答案
- B.让路由切换动画更流畅
- C.提高代码可读性
- D.修复路由守卫的Bug
答案:A。懒加载把每个路由对应的组件拆成独立chunk,首屏只加载核心框架和当前页,因此能显著减少首屏加载时间。它与切换动画、代码可读性、守卫Bug都没有直接关系。
Q6. 关于前端路由守卫的实现,下列说法中可能导致运行时问题的是?
- A.在守卫中直接调用异步API但没有处理Loading状态← 正确答案
- B.守卫逻辑中无条件调用
next('/login')造成循环跳转← 正确答案 - C.使用
localStorage保存用户登录态 - D.在守卫中只检查前端角色而不校验后端权限← 正确答案
答案:ABD。A会导致多次快速导航时竞态,返回顺序错乱;B中若访问 /login 仍触发守卫并再次 next('/login'),会无限重定向;D将前端守卫当成安全边界,攻击者可直接构造请求访问后端接口;C是合法的常见做法,不算问题。
Q7. 用户在SPA列表中往下滚了很远,点击进入详情再返回列表,期望恢复滚动位置。Vue Router中应利用哪个配置?
- A.
scrollBehavior← 正确答案 - B.
beforeEach - C.
lazyLoad - D.
scrollToTop
答案:A。Vue Router的scrollBehavior(to, from, savedPosition)函数返回滚动位置对象,可用savedPosition恢复原位置。beforeEach是路由守卫,lazyLoad不是路由配置项,scrollToTop也不存在。
Q8. React Router v6中,使用createBrowserRouter和RouterProvider,关于路由错误处理说法正确的是?
- A.通过
errorElement渲染错误边界组件← 正确答案 - B.必须使用
componentDidCatch捕获路由错误 - C.路由错误在开发环境静默处理
- D.路由错误会直接导致浏览器白屏
答案:A。createBrowserRouter支持在路由配置中通过errorElement指定错误边界组件,当loader或render抛错时显示该组件。componentDidCatch是类组件的错误边界,但路由错误更适合用errorElement;C、D与框架行为不符。
Q9. 关于路由参数,下列说法正确的有?
- A.React中动态路由参数可通过
useParams获取← 正确答案 - B.Vue Router查询字符串位于
route.query← 正确答案 - C.路径参数只能出现在URL末尾
- D.查询参数与路径参数可同时存在← 正确答案
答案:ABD。React Router的useParams返回路径参数对象;Vue Router的route.query保存查询字符串;路径参数可以出现在任意位置,如/user/:id/posts;URL可以同时包含路径参数(/user/123)和查询参数(?tab=posts),因此C错。
Q10. 用户直接访问History模式的子路径,若服务器未配置重定向,会返回?
- A.404页面← 正确答案
- B.200页面
- C.index.html
- D.空白页
答案:A。History模式下,子路径在服务器上没有对应的物理文件,如果没配置try_files或类似规则,服务器会返回404。而Hash模式的hash部分不会发给服务器,所以不会出现这个问题。这就是History路由必须服务端配合的核心原因。