第98课:React Router v6(下)——嵌套路由、导航、useNavigate、路由守卫
上一课我们学习了 React Router v6 的路由配置基础——BrowserRouter、Routes、Route 和基本的导航组件。真实的应用远不止简单的“一对一”路径映射:页面通常由共享布局(如侧边栏 + 内容区)组成,不同子页面在布局内切换;用户登录后某些页面才可访问;表单提交后需要编程式跳转;URL 中的查询参数(如搜索关键词、分页)需要方便地读写。React Router v6 为这些场景提供了强大的工具:嵌套路由与 <Outlet> 实现布局复用、**useNavigate** 实现命令式导航、**useSearchParams** 管理查询字符串、以及通过路由守卫模式保护敏感页面。本节课将逐一深入这些进阶能力,并通过一个带认证的仪表板综合示例将它们串联起来。
1. 嵌套路由与共享布局:<Outlet>
1.1 为什么需要嵌套路由?
在典型的后台管理系统中,所有页面共享相同的侧边栏和顶栏,只有主内容区域随路由变化。如果没有嵌套路由,你需要在每个页面组件中手动引入布局组件,或者在路由配置中重复包裹布局——这既繁琐又容易出错。嵌套路由允许你定义一个父路由渲染共享布局,并在布局中预留一个“插槽”(<Outlet>),子路由匹配的内容会自动渲染到该插槽中。
1.2 <Outlet>:子路由的渲染占位符
<Outlet> 是 React Router v6 提供的组件,它标记了子路由内容应该出现的位置。父路由的 element 中渲染 <Outlet />,子路由通过嵌套 <Route> 来定义。
1 | import { BrowserRouter, Routes, Route, Outlet, Link } from 'react-router-dom'; |
关键规则:
- 父路由
element中必须包含<Outlet />,否则子路由内容不会显示。 - 子路由的
path是相对于父路由路径的。上例中path="users"实际匹配/dashboard/users,不需要写完整路径。 index子路由(不带path)在父路径精确匹配时渲染(如/dashboard精确匹配时渲染<DashboardHome />)。
1.3 多层嵌套
嵌套可以无限层级。例如,用户管理页面下还可以再嵌套用户详情:
1 | <Route path="/dashboard" element={<DashboardLayout />}> |
UsersLayout 内部同样渲染 <Outlet />,二级子路由的内容会渲染到该 <Outlet> 中。这种层层嵌套的结构使得 UI 层级与路由层级完全对应,代码组织清晰直观。
1.4 useOutletContext:向子路由传递数据
父路由组件可以向 <Outlet> 传递上下文数据,子路由组件通过 useOutletContext 接收。这避免了将数据通过 URL 参数或状态管理库传递的麻烦。
1 | // 父组件 |
2. useNavigate:编程式导航
声明式导航(<Link>、<NavLink>)适合用户点击链接的场景,但很多情况下你需要通过代码逻辑触发跳转——如表单提交成功后跳转到列表页、登录成功后跳转到主页、或是在异步操作完成后执行重定向。useNavigate 返回一个函数,用于命令式地改变 URL。
2.1 基本用法
1 | import { useNavigate } from 'react-router-dom'; |
navigate(to, options?) 的第一个参数与 <Link to> 相同,可以是字符串或对象。第二个可选参数 options 支持:
- **
replace: boolean**:如果为true,将替换当前历史记录条目(而非新增一个条目)。适用于登录后跳转(用户按“返回”时不应该回到登录页)。 - **
state: any**:携带任意状态数据,传递给目标路由,可通过useLocation().state获取。 - **
relative: 'route' | 'path'**:控制相对路径的解析方式(通常无需设置,默认行为已足够)。
1 | // 替换当前历史条目,用户无法通过“后退”回到此页 |
2.2 数字导航:navigate(-1)
与浏览器的 history.go() 类似,navigate 可以接收一个整数,表示在历史记录栈中前进或后退:
1 | <button onClick={() => navigate(-1)}>返回上一页</button> |
2.3 useNavigation:获取导航状态
useNavigation 返回当前导航状态,包含 { state, location, formData } 等字段。state 可以是 'idle'、'loading' 或 'submitting'。它适用于在数据加载期间显示全局进度条或加载指示器。
1 | import { useNavigation } from 'react-router-dom'; |
3. useSearchParams:读写查询参数
查询字符串(?page=2&sort=asc)是前端页面传递筛选、分页、排序等状态的标准方式。React Router v6 提供 useSearchParams,它的 API 与 useState 非常相似——返回一个包含当前查询参数和更新函数的数组。
3.1 读取查询参数
1 | import { useSearchParams } from 'react-router-dom'; |
searchParams 是一个 URLSearchParams 实例,拥有 get(key)、getAll(key)、has(key)、toString() 等方法。它不是普通对象,不能直接解构。
3.2 更新查询参数
setSearchParams 类似 setState,接收一个新的查询参数对象(或函数),并自动更新 URL(不会导致页面重新加载)。React Router 会保持组件树中相关状态的一致性。
1 | function Pagination() { |
注意:setSearchParams 与 useState 的 setter 一样,支持传入函数来基于前一个值更新。但注意要使用 prev 对象上的方法(set、delete)修改,并返回 prev 自身或一个新的 URLSearchParams。
3.3 useLocation:获取完整的路径信息
如果你需要读取当前路径的 pathname、search(原始查询字符串)、hash(锚点)或 state(通过 navigate 传递的状态),可以使用 useLocation。
1 | import { useLocation } from 'react-router-dom'; |
location.state 常用于接收导航时通过 navigate('/path', { state: { from: 'login' } }) 携带的上下文数据。
4. 路由守卫:保护需要认证的页面
大多数应用都有认证保护的需求——未登录用户访问 /dashboard 时应被重定向到 /login,登录成功后再回到之前想去的页面。React Router 没有内置的“路由守卫”组件(不像 Vue Router 有 beforeEnter),但可以通过封装一个 <ProtectedRoute> 组件来实现。
4.1 实现 <ProtectedRoute>
1 | import { Navigate, useLocation } from 'react-router-dom'; |
关键点:
<Navigate>是一个 React Router 组件,渲染它会导致立即重定向(在渲染阶段完成,无需等待副作用)。replace属性确保重定向替换当前历史记录条目,用户按“后退”时不会回到空白页。state={{ from: location }}将用户原本想访问的页面地址传递给登录页,登录页在登录成功后可以通过navigate(location.state?.from?.pathname || '/', { replace: true })跳回。
4.2 在路由中使用
1 | <Routes> |
注意:/dashboard/* 中的 * 确保所有以 /dashboard 开头的子路由都会经过 ProtectedRoute 检查。若未登录,整个 <DashboardLayout> 及其子路由都不会渲染,直接重定向到登录页。
4.3 基于角色的守卫扩展
你可以将 ProtectedRoute 扩展为接收 allowedRoles prop,实现更细粒度的权限控制:
1 | function ProtectedRoute({ children, allowedRoles }) { |
5. 综合实战:带认证的仪表板应用
下面将嵌套路由、useNavigate、useSearchParams、ProtectedRoute 和 useOutletContext 整合为一个完整的仪表板应用。
1 | // ---- AuthContext.jsx ---- |
流程解读:
- 用户访问
/dashboard→ProtectedRoute检查user为null→ 重定向到/login,携带state.from。 - 用户在登录页输入用户名并提交 →
setUser更新 Context →navigate(from, { replace: true })跳回原本想去的页面。 DashboardLayout通过Outlet渲染子路由,并通过useOutletContext向子组件传递user对象。- 退出登录时
setUser(null)并navigate('/login', { replace: true })清除历史栈。
课后练习
一、概念自测(选择题 / 填空题)
(单选) React Router v6 中,哪个组件用于渲染嵌套子路由的内容?
A.<Children>
B.<RouteChildren>
C.<Outlet>
D.<NestedRoute>(单选)
useNavigate返回的导航函数不能执行以下哪项操作?
A. 跳转到指定路径'/dashboard'。
B. 传递状态对象{ from: '/login' }。
C. 跳转到绝对外部 URL'https://example.com'。
D. 后退到上一页(navigate(-1))。(填空) 要读取当前 URL 中的查询参数
?page=2&sort=desc,应使用useSearchParamsHook,并通过返回的searchParams.______('page')获取特定参数的值。(多选) 以下关于
<ProtectedRoute>路由守卫的描述,哪些是正确的?
A. 它通常使用<Navigate>组件实现未认证时的重定向。
B. 它可以接收allowedRoles之类的 props 来实现基于角色的权限控制。
C. 它是 React Router v6 内置的组件,可直接从react-router-dom导入。
D. 它的实现原理依赖于条件渲染——满足条件时返回children,否则返回<Navigate>。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要实现一个购物应用的订单流程,包含以下路由和功能:
/products:商品列表页(通过useSearchParams支持分页参数?page=1和分类筛选?category=electronics)。/cart:购物车页面(受保护路由,未登录用户重定向到/login)。/checkout:结算页面(受保护路由)。/login:登录页面(登录成功后跳转到登录前尝试访问的页面,或默认跳转到/products)。/order-confirmation:下单成功后的确认页面(通过navigate从结算页跳转,并携带状态{ orderId: 12345 })。- 提供一个上下文
AuthContext管理user状态,以及ProtectedRoute组件。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成完整的 React 路由配置及相关组件代码。提示词中需明确指定嵌套路由、useNavigate、useSearchParams 和路由守卫的使用方式。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请为一个购物应用创建完整的路由和认证流程。需要创建以下文件:
src/contexts/AuthContext.jsx:使用createContext、useState、useMemo创建AuthProvider和useAuthHook,管理user状态。src/components/ProtectedRoute.jsx:接收children,使用useAuth判断登录状态。未登录时返回<Navigate to="/login" state={{ from: location }} replace />,否则返回children。src/pages/LoginPage.jsx:使用useAuth的setUser登录,使用useNavigate和useLocation跳转到from路径(若不存在则/products)。src/pages/ProductList.jsx:使用useSearchParams获取page和category参数,展示模拟商品列表,渲染分页和分类筛选链接(通过setSearchParams更新参数)。src/pages/Cart.jsx、src/pages/Checkout.jsx、src/pages/OrderConfirmation.jsx:分别实现购物车、结算页(提交后navigate('/order-confirmation', { state: { orderId: Date.now() } }))、确认页(通过useLocation().state显示订单 ID)。src/App.jsx:配置BrowserRouter、AuthProvider,定义所有路由,其中/cart和/checkout包裹<ProtectedRoute>。还要配置一个*404 页面。- 所有组件添加简洁的 JSDoc 注释。完成后列出所有文件内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请解释 React Router v6 中嵌套路由的实现方式,以及
<Outlet>的作用。如何实现一个路由守卫(Protected Route)来保护需要认证的页面?如果用户在未登录时访问受保护页面,如何让他在登录后自动跳转回之前想去的页面?请结合Navigate、useLocation和useNavigate说明。
参考答案:
React Router v6 中嵌套路由通过将 <Route> 组件嵌套在父 <Route> 内部来实现。父路由的 element 中渲染 <Outlet />,子路由匹配的内容会自动渲染到该 <Outlet> 位置。这种方式使得布局(如侧边栏、顶栏)可以定义在父路由组件中,而无需在每个子页面重复包裹。
路由守卫通常封装为一个组件,例如 <ProtectedRoute>,它接收 children。内部通过自定义 Hook(如 useAuth)判断用户是否已登录:若已登录,直接返回 children;若未登录,返回 <Navigate to="/login" state={{ from: location }} replace />。其中 location 通过 useLocation 获取,保存了用户原本想访问的路径。登录页在登录成功后通过 useNavigate 和 location.state?.from?.pathname 获取目标路径,并执行 navigate(from, { replace: true }),从而实现登录后跳回原始页面。replace: true 确保重定向和登录跳转不会在历史记录中留下多余条目。
课后练习答案
一、概念自测答案
C
- 解析:
<Outlet>是嵌套路由中子路由内容的渲染占位符。A、B、D 均不是 React Router v6 的 API。
- 解析:
C
- 解析:
useNavigate只能进行应用内部的导航,不能直接跳转到外部绝对 URL(应使用window.location.href或<a>标签)。A、B、D 均为useNavigate支持的功能。
- 解析:
get- 解析:
searchParams.get('page')返回对应参数的值(字符串),如果没有则返回null。
- 解析:
A、B、D
- 解析:C 错误,
ProtectedRoute不是 React Router v6 内置组件,需要开发者自己封装。A(使用<Navigate>重定向)、B(可扩展allowedRoles)、D(条件渲染模式)均为正确描述。
- 解析:C 错误,
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请使用 React Router v6 创建一个购物应用的订单流程。要求:
AuthContext管理登录状态,ProtectedRoute组件包裹受保护路由(/cart、/checkout)。LoginPage登录后使用navigate(from, { replace: true })跳回原页面。ProductList使用useSearchParams读取和更新page、category参数。Checkout提交后跳转到/order-confirmation,携带state: { orderId }。- 404 页面处理,所有路由通过
BrowserRouter配置。- 输出所有组件的完整代码。”