← 全部材料 路由
ROUTING · 11 个高频考点

路由就是URL 的翻译器

路由题看着细碎,其实都能归到三个问题:URL 怎么变、变完怎么匹配、匹配完在哪渲染。这份按这三个问题组织,把 hash 与 history 的分工、守卫的顺序、动态路由的坑讲透。

01先分清模式hash 和 history 的差别不在写法,在"服务端能不能看见路径"。
02记顺序不记名字守卫按"先离开、再全局、再局部"排,比背七个名字好记。
03认准复用还是重建参数变化时组件默认复用,这是大半路由 bug 的来源。
1

路由的本质:URL 到组件树的映射★

先想清楚"状态存在哪"
前端路由要解决一件事:URL 变了但不刷新页面。做法是接管 URL 变化,把 URL 解析成一条路由记录,再渲染对应组件。所以核心是三块——监听 URL、匹配规则、渲染出口。所有框架都是这三块的不同实现。
◐ 可交互:输一个路径,看它命中哪条路由、服务端收到什么
试试 /user/42、/user/42/edit、/nope,再切一下模式,看服务端各收到什么。
一个最小的前端路由,不含任何框架// 1) 路由表 const routes = [ { path: '/', render: () => '首页' }, { path: '/about', render: () => '关于' }, { path: '/user/:id',render: (p) => '用户 ' + p.id }, ]; // 2) 匹配:把 /user/42 变成 { path:'/user/:id', params:{ id:'42' } } function match(pathname) { for (const r of routes) { const keys = []; const pattern = r.path.replace(/:([^/]+)/g, (_, k) => { keys.push(k); return '([^/]+)'; }); const m = pathname.match(new RegExp('^' + pattern + '$')); if (m) { const params = {}; keys.forEach((k, i) => params[k] = decodeURIComponent(m[i + 1])); return { route: r, params }; } } return null; } // 3) 渲染 function render() { const hit = match(location.pathname); document.getElementById('app').textContent = hit ? hit.route.render(hit.params) : '404'; } // 4) 监听:两种模式在这里分叉 window.addEventListener('popstate', render); // 前进后退 render(); // 5) 跳转:不能改 location.pathname(会刷新),要用 pushState function push(path) { history.pushState({}, '', path); render(); // pushState 不触发事件,要手动调 }
面试怎么答前端路由本质是把 URL 映射到组件树。三块职责:监听 URL 变化、按规则匹配出路由记录和参数、在出口处渲染。hash 模式靠监听 hashchange,改 hash 不请求服务端;history 模式靠 pushState 改 URL 加 popstate 监听前进后退,但它需要服务端把所有路径都回退到 index.html,否则刷新会 404。
2

hash 模式:为什么它天然不会 404★

# 后面的东西服务端看不见
URL 里 # 之后的部分叫 fragment,规范里它不参与 HTTP 请求——浏览器发请求时会把它去掉。所以 /a#/user/1 和 /a#/user/2 请求的都是 /a,服务端不用做任何配置。

hash 的优势

  • 服务端零配置,刷新、直接访问都正常
  • 兼容到很老的浏览器
  • 不需要后端配合,静态托管就能用

hash 的代价

  • URL 里有个 #,不好看,也不符合"一个 URL 一个资源"
  • 服务端拿不到 # 后面的内容,做不了根据路径的 SSR 或埋点
  • 部分第三方回调、分享卡片对 # 处理不一致
面试怎么答hash 模式利用 URL 里 # 后面的部分不参与 HTTP 请求这个特性,所以服务端永远只收到 # 前面的路径,不需要任何配置,刷新和直接访问都不会 404。缺点一是 URL 不好看,二是服务端拿不到路由信息,做不了基于路径的 SSR 和统计。适合纯静态托管、不能改服务端配置的场景。
3

history 模式:pushState 与 popstate★

干净 URL 的代价在服务端
history 模式用 pushState / replaceState 改 URL(不刷新),用 popstate 感知前进后退。代价是服务端必须把未知路径回退到 index.html,否则用户刷新 /user/42 时服务端会去找这个真实文件,返回 404。
服务端必须配合:把未命中的路径回退到入口文件【Nginx】 location / { try_files $uri $uri/ /index.html; } 【Apache】 RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] 【Node / Express】 app.get('*', (req, res) => res.sendFile(path.join(dist, 'index.html'))); 注意要放在所有 API 路由之后,否则会吃掉 API 请求 【静态托管】 大部分平台支持配置 rewrite / fallback, 没有这个选项就只能退回 hash 模式。
面试怎么答history 模式用 pushState 和 replaceState 改变 URL 而不刷新,用 popstate 监听前进后退。要注意 pushState 本身不触发 popstate,所以封装跳转时要手动调一次渲染。最大的代价在服务端:因为 /user/42 这种路径在磁盘上不存在,刷新会 404,必须配置 try_files 把所有未命中的请求回退到 index.html,并且 API 路由要放在前面,否则会被吃掉。
4

动态路由与参数

params 和 query 不是一回事
params 是路径的一部分(/user/:id),query 是 ? 后面的(?page=2)。两者最大的区别是:params 变化会复用同一个组件实例,query 变化也是——但很多人误以为组件会重建,于是在 onMounted 里取参数,结果拿到旧值。
面试怎么答params 是路径的一部分,query 是问号后面的。这两个变化时,框架默认复用同一个组件实例而不是重建,所以只在 mounted 里读参数会拿到旧值,必须用 watch 或 useEffect 监听参数变化。如果确实需要重建,可以给 router-view 加 key 等于 fullPath,但代价是会丢掉所有组件内部状态。
5

嵌套路由:router-view 的递归

父子出口怎么对上
嵌套路由的本质是父组件里放一个出口,子路由渲染到那个出口里。所以父组件必须包含 <router-view />(React 里是 <Outlet />),否则子路由匹配成功但没地方渲染——这是最常见的"路由配了但页面空白"。
React Router 的同样结构<Routes> <Route path="/settings" element={<SettingsLayout />}> <Route index element={<Profile />} /> {/* /settings */} <Route path="account" element={<Account />} /> {/* /settings/account */} <Route path="theme" element={<Theme />} /> {/* /settings/theme */} </Route> </Routes> // SettingsLayout 里必须渲染 <Outlet />,否则子路由白屏
面试怎么答嵌套路由是父组件里放一个出口,子路由渲染到那个出口里。所以父组件必须渲染 router-view 或 Outlet,否则会匹配成功但页面空白,这是最常见的排查点。子路由的 path 不要写开头的斜杠,写了会被当成绝对路径。空路径的子路由用来指定父路径本身渲染什么。
6

路由守卫:完整执行顺序★

背不下来就记"先全局后局部"
Vue Router 的守卫分五类,执行顺序固定:组件内离开 → 全局 beforeEach → 路由独享 beforeEnter → 组件内 beforeRouteEnter → 全局 beforeResolve → 全局 afterEach → 组件挂载。记法:离开的、全局的、路由的、组件的、解析后的、收尾的。
Vue Router 4 的完整顺序导航从 /a 到 /b 1. beforeRouteLeave (/a 组件内) ← 用户离开当前页 2. beforeEach (全局) ← 最常用的鉴权位置 3. beforeRouteUpdate (/b 组件复用时才触发) 4. beforeEnter (/b 路由独享) 5. beforeRouteEnter (/b 组件内,此时拿不到 this) 6. beforeResolve (全局) ← 所有异步守卫完成后 7. afterEach (全局) ← 导航已确认,收尾用 8. DOM 更新 9. beforeRouteEnter 的 next 回调(此时才有 this) 被取消或重定向时,走不到 7 之后的步骤。
面试怎么答Vue Router 的守卫顺序是:先组件内的 beforeRouteLeave,再全局 beforeEach,然后路由独享的 beforeEnter、组件内的 beforeRouteEnter、全局的 beforeResolve,最后 afterEach 收尾。记住先离开再进入、全局包着局部。鉴权一般放在全局 beforeEach,返回路由地址做重定向,返回 false 取消导航。React Router 没有守卫,是用包装组件加 Navigate 或者在 loader 里 throw redirect 来实现。
7

React Router v6:从组件路由到数据路由

loader 把取数挪出了组件
v6 之前是"渲染时才取数"(useEffect 里 fetch),于是必然出现"渲染空态 → 请求 → 再渲染"。v6.4 引入 loader:导航发生前先取数,数据准备好再渲染,等效于把取数提前到了路由层。

useEffect 取数

  • 组件先渲染一次空态,再请求,再渲染
  • 请求瀑布:父子组件串行发请求
  • 错误和 loading 都要手写状态
  • 导航过程中无法取消

loader 取数

  • 导航确认后先取数,数据到了才渲染
  • 同级路由的 loader 并行执行
  • 错误交给 errorElement,不用手写
  • 导航被取消时请求也会中断
面试怎么答React Router v6.4 之后引入了 loader,把数据获取提到路由层:导航发生时先执行 loader,数据准备好再渲染组件,所以不会出现空态闪烁,同级路由的 loader 还会并行执行。错误统一交给 errorElement,导航取消时请求也会中断。要注意 loader 是普通函数不能用 Hook,而且它只在导航时跑,不响应组件状态变化。
8

路由懒加载:分包点选在哪

一个 import() 就是一个 chunk
路由懒加载就是把 import Component from './X' 换成 () => import('./X')。每个动态 import 就是一个独立 chunk,所以拆得越细首屏越小,但请求数越多。经验值是按"页面"切,不按"组件"切。
拆太细的代价按页面拆(推荐) index.js 180KB home.js 40KB detail.js 55KB 首屏 220KB,切页面再拉 40-55KB 按组件拆(常见错误) index.js 60KB 30 个 chunk 各 3-8KB 首屏 60KB 但要发 12 个请求(首页用到的组件) 结论:HTTP/2 下多请求不算致命,但每个 chunk 都有 解析和执行开销,还有依赖被重复打进多个 chunk 的风险。 切分粒度应该是"会一起出现的代码"。
面试怎么答路由懒加载就是把静态 import 换成动态 import,每个动态 import 是一个独立 chunk。切分粒度按页面而不是按组件,因为会一起出现的代码拆开只会增加请求数和重复依赖。真正提升体验的是预加载:用户停留一会就提前把下一页拉下来。还要处理发版后旧 chunk 被删导致的 Loading chunk failed,监听加载错误后强制刷新。
9

权限路由:前端过滤还是动态注册★

两种做法差别在"什么时候知道有权限"
两种主流方案:静态全量路由 + 前端过滤(简单,但路由表打包进了产物,能被人翻出来)、后端返回菜单 + 动态 addRoute(安全,但要注意刷新后重建和 404 兜底顺序)。企业后台基本用后者。
面试怎么答权限路由有两种做法。一是路由全量注册,在全局守卫里判断 meta 上的角色,不满足就跳 403,优点是简单,缺点是完整路由表在产物里能被看到。二是登录后从后端拉菜单,转成路由记录用 addRoute 动态注册,更安全。动态注册要注意三个坑:404 兜底路由必须最后注册、刷新会丢动态路由所以在启动时要重建、切换账号要清掉旧路由。
10

滚动行为与页面缓存

返回列表页位置丢了,怎么办
两件事要分开:滚动位置用框架的滚动行为配置恢复;组件状态(筛选条件、已加载的数据)要靠缓存组件,Vue 是 keep-alive,React 需要把状态提到上层或者自建缓存。
Vue keep-alive 的两个坑<keep-alive :include="['ListPage']"> <router-view /> </keep-alive> 坑 1:include 匹配的是组件的 name <script setup> 里没有 name,要用 defineOptions({ name: 'ListPage' }) 或者文件名推导(Vue 3.3+ 的 defineOptions) 坑 2:从详情页返回时数据不刷新 因为组件没销毁,onMounted 不再执行 解法:onActivated 里判断是否需要重新拉数据 坑 3:同一个组件不同参数被复用 /list?page=1 和 /list?page=2 会命中同一个缓存实例 需要 key 区分,或者 onBeforeRouteUpdate 里手动处理
面试怎么答返回列表页要恢复两样东西:滚动位置和组件状态。滚动位置用路由的 scrollBehavior,有 savedPosition 时返回它,新页面回顶部。组件状态在 Vue 里用 keep-alive 缓存,要注意 include 匹配的是组件 name,script setup 里需要用 defineOptions 声明;缓存之后 onMounted 不再执行,取数要挪到 onActivated。React 没有内置的缓存方案,通常把状态提到上层或者用状态管理保存。
11

路由设计的几个判断题

面试常问的"你会怎么组织"
路由设计有三组常见取舍:状态放 URL 还是放内存、路由层级按页面还是按布局、路由参数放路径还是放 query。答案不是固定的,判断标准是这个状态需不需要被分享、被刷新保留、被埋点统计。
状态放哪:三个判断标准能被分享 / 刷新要保留 / 要埋点统计 → 放 URL(query 或 params) 举个具体的: 列表筛选条件(状态、日期、关键词) → 放 query,可分享 列表页码 / 每页条数 → 放 query 弹窗打开与否 → 放内存(除非要深链) 表格列宽、展开行 → 放 localStorage 当前 tab → 放 query(能深链到某个 tab) 反面例子:把筛选条件放 store 里, 用户复制链接发给同事,打开是默认列表。
面试怎么答路由设计的核心判断是"这个状态该不该放进 URL"。能被分享、刷新要保留、要埋点统计的,都放 URL,比如列表的筛选条件和页码;纯交互的临时状态放内存。路径参数用来标识资源,query 用来表达视图修饰。层级不超过三层,再深就用 query 展开。这样用户按返回、复制链接都能回到原样。
12

速答清单

考前十分钟只看这一页
01前端路由的本质是什么?
把 URL 映射到组件树。三块职责:监听 URL 变化、按规则匹配出路由记录和参数、在出口处渲染。
02hash 模式为什么不会 404?
# 后面的 fragment 不参与 HTTP 请求,服务端永远只收到 # 前面的路径,所以不需要任何配置。
03history 模式为什么需要服务端配合?
/user/42 在磁盘上不存在,刷新时服务端找不到文件就 404。要配置 try_files 把所有未命中路径回退到 index.html。
04pushState 会触发 popstate 吗?
不会。只有用户点前进后退才触发。所以自己封装的跳转函数要手动调用一次渲染。
05从 /user/1 跳到 /user/2,组件会重建吗?
不会,复用同一个实例。所以要 watch 参数变化,不能只在 mounted 里取一次。需要重建给 router-view 加 key="fullPath"。
06嵌套路由页面空白怎么排查?
父组件里没有渲染 router-view 或 Outlet。子路由匹配成功但没地方渲染。
07Vue Router 守卫的执行顺序?
组件内 beforeRouteLeave → 全局 beforeEach → 路由独享 beforeEnter → 组件内 beforeRouteEnter → 全局 beforeResolve → 全局 afterEach → DOM 更新。
08beforeRouteEnter 里为什么拿不到 this?
组件还没创建。要在 next(vm => ...) 回调里访问,或者用 setup 里的 onBeforeRouteEnter。
09React Router v6 的 loader 解决了什么?
把取数提到路由层:导航时先执行 loader,数据准备好再渲染,避免空态闪烁;同级 loader 并行执行,错误统一交给 errorElement。
10路由懒加载怎么切才合理?
按页面切,不按组件切。切分粒度应该是"会一起出现的代码",首屏路由不要懒加载。
11发版后点菜单报 Loading chunk failed,原因?
旧 chunk 文件已被删除,用户还在旧页面。监听加载错误事件后 location.reload() 强制刷新。
12动态加路由时 404 兜底路由要注意什么?
必须最后注册。先注册的话动态路由永远匹配不到,刷新直接跳 404。
13刷新后动态路由丢失怎么办?
addRoute 只在内存里。要在应用启动时、路由就绪前重新拉菜单重建路由表。
14返回列表页怎么恢复滚动位置?
用路由的 scrollBehavior,savedPosition 存在时返回它,新页面返回 top: 0。
15keep-alive 缓存后数据不刷新怎么解?
onMounted 不再执行,把取数逻辑挪到 onActivated 里,或者在里面判断是否需要重新请求。
16筛选条件该放 URL 还是 store?
放 URL 的 query。能被分享、刷新要保留、要埋点统计的都该放 URL,否则复制链接给别人打开就是默认状态。