一、基础概念类
1. React Router 的核心作用是什么?
React Router 是 React 生态中用于 SPA(单页应用)路由管理 的库,核心作用:
- 实现 URL 与组件的映射关系(路由匹配);
- 提供无刷新的页面跳转(客户端路由);
- 支持动态路由、嵌套路由、路由鉴权等高级功能;
- 维护路由状态(参数、历史记录),实现组件间路由相关通信。
2. BrowserRouter 和 HashRouter 的区别?
两者是 React Router 的顶层路由容器,核心差异在于 URL 形式 和 底层实现原理:
| 对比维度 | BrowserRouter(history 模式) | HashRouter(hash 模式) |
|---|---|---|
| URL 形式 | http://xxx.com/user/123(无 #) | http://xxx.com/#/user/123(带 #) |
| 底层原理 | 基于 HTML5 的 history API(pushState/replaceState) | 基于 URL 中的 hash(锚点),依赖 hashchange 事件 |
| 后端配置 | 需要配置(Nginx/Apache),否则刷新 404 | 无需后端配置,刷新不会 404 |
| 兼容性 | 支持 HTML5 的现代浏览器(IE10+) | 兼容性更强(支持 IE8+) |
| 场景 | 正式生产环境(需要后端配合) | 快速开发、静态页面部署(无需后端) |
注意:BrowserRouter 刷新 404 原因:URL 被后端解析为真实接口路径,需配置后端将所有路由转发到
index.html(如 Nginx 的try_files $uri $uri/ /index.html;)。
3. Link、NavLink、a 标签的区别?
| 标签 | 核心作用 | 关键差异 | 场景 |
|---|---|---|---|
<a> | 原生页面跳转 | 会刷新页面,破坏 SPA 无刷新特性 | 跳转到外部网站 |
<Link> | React Router 内部跳转 | 无刷新跳转,通过 to 属性指定目标路由 | 普通内部页面跳转 |
<NavLink> | 带激活状态的内部跳转 | 继承 <Link> 功能,可通过 className/style 配置激活样式 | 导航菜单(如侧边栏) |
代码示例(v6):
jsx
// NavLink 激活状态配置
<NavLink
to="/home"
// 函数形式:接收 isActive 参数,动态设置类名
className={({ isActive }) => isActive ? 'nav-active' : 'nav-normal'}
>
首页
</NavLink>
// 样式效果:激活时添加 nav-active 类
.nav-active { color: red; font-weight: bold; }
4. 什么是动态路由?如何实现?
动态路由:URL 中包含可变参数的路由(如 /user/123、/post/456),用于匹配一类页面(如不同用户的详情页)。
实现步骤(v6):
- 路由定义时,用
:参数名声明动态参数; - 组件中通过
useParams()钩子获取参数。
代码示例:
jsx
// 1. 路由配置(动态参数 :id)
<Routes>
<Route path="/user/:id" element={<UserDetail />} />
</Routes>
// 2. 组件中获取参数
import { useParams } from 'react-router-dom';
function UserDetail() {
const { id } = useParams(); // 解构动态参数 id
return <div>用户 ID:{id}</div>;
}
5. v6 中 <Routes> 和 v5 中 <Switch> 的区别?
v6 用 <Routes> 替代了 v5 的 <Switch>,核心差异:
| 特性 | v5 <Switch> | v6 <Routes> |
|---|---|---|
| 匹配规则 | 模糊匹配(需手动加 exact 实现精确匹配) | 默认 精确匹配(无需 exact),模糊匹配需用 * |
| 路由嵌套 | 需手动嵌套 <Route> | 支持扁平化配置,配合 <Outlet> 实现嵌套路由 |
| 性能 | 遍历所有子路由,找到第一个匹配项 | 优化匹配逻辑,只渲染匹配的路由组件 |
| 子路由渲染 | 无默认出口,需手动渲染子组件 | 必须通过 <Outlet> 渲染子路由组件 |
示例对比:
jsx
// v5(需 exact 实现精确匹配)
<Switch>
<Route exact path="/" component={Home} />
<Route path="/user" component={UserList} />
<Route path="/user/:id" component={UserDetail} />
</Switch>
// v6(默认精确匹配,无需 exact)
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user" element={<UserList />} />
<Route path="/user/:id" element={<UserDetail />} />
</Routes>
二、进阶用法类
6. 如何获取路由参数?(params、search、state)
React Router 支持 3 类路由参数,适用场景不同:
| 参数类型 | 获取方式 | URL 表现 | 适用场景 |
|---|---|---|---|
| params | useParams() | /user/:id(路径参数) | 唯一标识(如用户 ID) |
| search | useSearchParams() | ?name=xxx&age=20(查询参数) | 筛选、分页(如列表筛选) |
| state | useLocation().state | 隐藏在历史记录中(URL 无显示) | 传递敏感数据、临时状态(如登录后回跳路径) |
代码示例:
jsx
// 1. params(动态路径参数)
const { id } = useParams();
// 2. search(查询参数)
const [searchParams, setSearchParams] = useSearchParams();
const name = searchParams.get('name'); // 获取
setSearchParams({ name: 'new', age: 25 }); // 修改
// 3. state(历史记录状态)
// 跳转时传递
<Link to="/user" state={{ from: '/home' }} />
// 或编程式导航
navigate('/user', { state: { from: '/home' } });
// 组件中获取
const { state } = useLocation();
const from = state?.from;
7. 编程式导航如何实现?(v5 vs v6)
编程式导航指通过代码(而非 <Link> 标签)实现页面跳转,核心差异在于 v6 用 useNavigate 替代了 v5 的 useHistory。
v6 实现(useNavigate):
jsx
import { useNavigate } from 'react-router-dom';
function Demo() {
const navigate = useNavigate();
const handleGo = () => {
navigate('/home'); // 跳转到 /home(push 模式,新增历史记录)
navigate(-1); // 后退 1 步(类似 history.back())
navigate(1); // 前进 1 步(类似 history.forward())
navigate('/user', { replace: true }); // replace 模式(替换当前历史记录,无法回退)
navigate('/user', { state: { id: 123 } }); // 传递 state
};
return <button onClick={handleGo}>跳转</button>;
}
v5 实现(useHistory):
jsx
import { useHistory } from 'react-router-dom';
const history = useHistory();
history.push('/home'); // 跳转
history.replace('/user'); // 替换
history.goBack(); // 后退
history.push('/user', { id: 123 }); // 传递 state
8. 如何实现嵌套路由?(v6 核心)
嵌套路由指路由层级嵌套(如 /dashboard/settings 是 /dashboard 的子路由),v6 通过 <Outlet> 组件实现子路由渲染,比 v5 更简洁。
实现步骤:
- 父路由配置时,不指定
path或指定父路径,子路由通过children嵌套; - 父组件中用
<Outlet>作为子路由的 “出口”(子路由组件会渲染在<Outlet>位置)。
代码示例:
jsx
// 1. 路由配置(嵌套结构)
<Routes>
{/* 父路由:路径 /dashboard,子路由嵌套在 children 中 */}
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} /> {/* 默认子路由(/dashboard) */}
<Route path="settings" element={<DashboardSettings />} /> {/* 子路由(/dashboard/settings) */}
<Route path="profile" element={<DashboardProfile />} /> {/* 子路由(/dashboard/profile) */}
</Route>
</Routes>
// 2. 父组件 Dashboard(用 <Outlet> 渲染子路由)
function Dashboard() {
return (
<div>
<h1>仪表盘</h1>
<nav>
<Link to="settings">设置</Link> {/* 相对路径(无需写 /dashboard) */}
<Link to="profile">个人资料</Link>
</nav>
<Outlet /> {/* 子路由组件会渲染在这里 */}
</div>
);
}
注意:子路由的
path是相对路径(无需带父路径),<Link>中to也支持相对路径。
9. 如何实现路由鉴权(登录守卫)?
路由鉴权指:未登录用户访问需要权限的页面时,自动跳转到登录页;已登录用户则正常访问。v6 推荐用 <Outlet> + 鉴权组件实现。
实现步骤:
- 创建鉴权组件
RequireAuth,判断登录状态; - 需鉴权的路由嵌套在
RequireAuth组件中; - 未登录时跳转到登录页,并记录回跳路径。
代码示例:
jsx
// 1. 鉴权组件 RequireAuth
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux'; // 假设用 Redux 存储登录状态
function RequireAuth() {
const isLogin = useSelector(state => state.user.isLogin); // 获取登录状态
const location = useLocation(); // 获取当前访问路径
// 未登录:跳转到登录页,传递回跳路径(state.from)
if (!isLogin) {
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
// 已登录:渲染子路由(通过 Outlet)
return <Outlet />;
}
// 2. 路由配置(嵌套鉴权)
<Routes>
<Route path="/login" element={<Login />} />
{/* 需鉴权的路由:嵌套在 RequireAuth 中 */}
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>
</Routes>
// 3. 登录组件:登录成功后回跳
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from || '/'; // 回跳路径(默认首页)
const handleLogin = () => {
// 登录逻辑(如调用接口、存储 token)
navigate(from, { replace: true }); // 回跳原路径
};
return <button onClick={handleLogin}>登录</button>;
}
10. 如何配置 404 页面?
404 页面用于匹配所有未定义的路由,v6 中用 path="*" 实现(模糊匹配所有路径)。
代码示例:
jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserDetail />} />
{/* 404 页面:放在最后,匹配所有未定义路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
// NotFound 组件
function NotFound() {
return (
<div>
<h1>404 Not Found</h1>
<Link to="/">返回首页</Link>
</div>
);
}
11. 路由懒加载的实现方式?
路由懒加载指:组件在路由匹配时才动态加载(而非初始加载时全部加载),优化首屏加载速度。核心依赖 React.lazy + Suspense。
实现步骤:
- 用
React.lazy(() => import('组件路径'))动态导入组件; - 用
Suspense包裹懒加载路由,指定加载时的 fallback(占位符)。
代码示例:
jsx
import { Routes, Route, Suspense } from 'react-router-dom';
import React from 'react';
// 懒加载组件(动态导入)
const User = React.lazy(() => import('./User'));
const Dashboard = React.lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}> {/* 加载时显示占位符 */}
<Routes>
<Route path="/user" element={<User />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
注意:
Suspense需包裹在懒加载路由的父级(可全局包裹,也可局部包裹),否则会报错。
12. 如何监听路由变化?
监听路由变化可用于:页面埋点、数据刷新、滚动重置等场景,核心通过 useLocation + useEffect 实现。
代码示例:
jsx
import { useLocation, useNavigate } from 'react-router-dom';
function Demo() {
const location = useLocation();
const navigate = useNavigate();
// 监听路由变化(pathname 或 search 改变时触发)
useEffect(() => {
console.log('当前路由:', location.pathname);
// 场景1:路由变化时重置滚动位置
window.scrollTo(0, 0);
// 场景2:埋点上报
// reportTrack('page_view', { path: location.pathname });
}, [location]); // 依赖 location(路由变化时 location 会更新)
return <div>监听路由变化示例</div>;
}
三、原理类
13. 前端路由(SPA 路由)的实现原理是什么?
SPA(单页应用)的核心是 “无刷新跳转”,前端路由通过 URL 变化触发组件更新,无需向后端请求新页面,原理分两类:
1. Hash 模式(HashRouter):
- 基于 URL 中的
hash(如#/user),hash变化不会触发页面刷新,也不会发送请求到后端; - 通过监听
window的hashchange事件,捕获hash变化,然后匹配对应的组件渲染。
2. History 模式(BrowserRouter):
- 基于 HTML5 的
historyAPI(pushState、replaceState),可修改 URL 但不刷新页面; - 通过监听
window的popstate事件(监听浏览器前进 / 后退按钮),配合historyAPI 手动修改 URL,实现路由切换; - 缺点:URL 变化时会触发后端请求,需后端配置所有路由转发到
index.html。
核心流程:
- 监听 URL 变化(
hashchange或popstate); - 解析 URL 路径,匹配预定义的路由规则;
- 渲染匹配的组件,隐藏不匹配的组件。
14. React Router 的核心原理是什么?
React Router 本质是 “组件化路由解决方案” ,核心基于 React 的上下文(Context)和 Hooks,流程如下:
- 路由上下文(RouterContext) :顶层
BrowserRouter/HashRouter会创建路由上下文,存储history、location等核心对象,子组件通过 Hooks(如useLocation、useNavigate)获取这些对象; - 路由匹配:
Routes组件遍历子Route,根据当前location.pathname匹配对应的Route,只渲染匹配的element; - 导航控制:
Link/useNavigate通过history对象修改 URL(pushState/hash),触发location更新; - 组件更新:
location是上下文状态,更新后会触发依赖它的组件(如Routes、useLocation所在组件)重新渲染,实现路由切换。
15. history 库的核心作用是什么?
React Router 依赖 history 库(而非原生 history API),核心作用:
- 封装
hash和history两种模式的路由操作(统一 API); - 提供
push、replace、go等方法,用于修改 URL 和历史记录; - 维护
location对象(包含pathname、search、state等),并在 URL 变化时通知 React Router 更新组件; - 处理浏览器前进 / 后退按钮的事件监听(
popstate/hashchange)。
四、v5 vs v6 核心差异(高频考点)
16. React Router v5 和 v6 的主要区别有哪些?
| 特性 | v5 | v6 |
|---|---|---|
| 顶层容器 | <BrowserRouter> 直接包裹路由 | 无变化,但路由配置需嵌套在 <Routes> 中 |
| 路由匹配容器 | <Switch> | <Routes>(默认精确匹配,性能优化) |
| 路由组件属性 | component/render | element(接收 JSX 元素,而非组件引用) |
| 嵌套路由 | 手动嵌套 <Route>,需写完整路径 | 扁平化配置(children 嵌套),配合 <Outlet> 渲染子路由 |
| 导航 Hooks | useHistory | useNavigate(API 更简洁) |
| NavLink 激活 | activeClassName/activeStyle | 需通过 className/style 接收函数(({ isActive }) => {}) |
| 路由匹配规则 | 模糊匹配(需 exact) | 默认精确匹配,模糊匹配需用 *(如 /user/*) |
| 路由配置 | 仅支持 JSX 配置 | 支持 JSX 配置和 useRoutes(对象配置) |
关键代码对比:
jsx
// v5 路由配置
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/user" component={UserList} />
<Route path="/user/:id" render={(props) => <UserDetail {...props} />} />
</Switch>
</BrowserRouter>
// v6 路由配置
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user" element={<UserList />} />
<Route path="/user/:id" element={<UserDetail />} />
</Routes>
</BrowserRouter>
五、实战场景类
17. 如何处理路由切换时的组件卸载(如数据清理)?
路由切换时,当前组件会卸载,需清理副作用(如定时器、订阅、请求取消),核心通过 useEffect 的返回函数实现。
代码示例:
jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
function UserDetail() {
const navigate = useNavigate();
let timer;
useEffect(() => {
// 副作用:开启定时器
timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 组件卸载时清理副作用(路由切换会触发卸载)
return () => {
clearInterval(timer); // 清除定时器
// 其他清理:取消请求、取消订阅等
// abortController.abort();
};
}, []);
return <div>用户详情页</div>;
}
18. 如何实现路由拦截(如权限粒度控制)?
除了登录鉴权,还可能需要更细粒度的权限控制(如角色权限:管理员可访问,普通用户不可),核心通过 “鉴权组件 + 角色判断” 实现。
代码示例:
jsx
// 角色鉴权组件(仅管理员可访问)
function RequireAdmin() {
const { userRole } = useSelector(state => state.user); // 角色:admin/normal
const location = useLocation();
if (userRole !== 'admin') {
return <Navigate to="/403" replace />; // 无权限跳 403
}
return <Outlet />;
}
// 路由配置
<Routes>
<Route element={<RequireAuth />}> {/* 登录鉴权 */}
<Route path="/dashboard" element={<Dashboard />} />
{/* 角色鉴权:仅管理员可访问 /admin */}
<Route element={<RequireAdmin />}>
<Route path="/admin" element={<AdminPanel />} />
</Route>
</Route>
<Route path="/403" element={<Forbidden />} /> {/* 无权限页面 */}
</Routes>
总结
React Router 面试核心围绕 v6 新特性(<Routes>、<Outlet>、useNavigate)、路由匹配规则、鉴权 / 懒加载 / 嵌套路由 等实战场景,以及 前端路由原理(hash/history 模式)。备考时需重点掌握 v6 与 v5 的差异,结合代码示例理解核心用法,同时理清底层原理(URL 监听、组件渲染流程)。