React: React Router 进阶
最后更新:2026-08-26
Tom 的基础路由已经跑通了,但新需求接踵而来:登录后需要自动跳转到仪表盘、某些页面需要登录才能访问、应用越来越大首页加载越来越慢、商品列表页需要支持 URL 中的筛选参数……他需要学习 React Router 的进阶功能来应对这些真实场景。
1. 你将学到
- useNavigate 编程式导航(登录跳转、后退前进)
- 路由守卫组件实现认证和权限控制
- React.lazy + Suspense 实现按需加载
- useSearchParams 管理 URL 查询参数
- 路由配置的组织与抽取策略
2. 概念图解
flowchart TD
U[用户操作] --> A{是否登录?}
A -->|未登录| B[ProtectedRoute<br/>→ 重定向到 /login]
A -->|已登录| C{有无权限?}
C -->|无权限| D[→ 403 页面]
C -->|有权限| E[加载目标页面]
E --> F{组件是否懒加载?}
F -->|是| G[Suspense<br/>显示 loading]
G --> H[渲染页面]
F -->|否| H
style A fill:#fff3e0,stroke:#f57c00
style B fill:#ffcdd2,stroke:#d32f2f
style G fill:#e1f5fe,stroke:#0288d1
style H fill:#e8f5e9,stroke:#388e3c
用户操作请求 → 路由守卫检查登录/权限 → 通过后加载页面(懒加载时显示 loading)→ 最终渲染。
3. 一个真实场景
Tom 的应用需要:登录成功后自动跳转到仪表盘、仪表盘页面需要登录保护、管理后台只有 admin 角色能访问、首页首屏加载速度要快(大页面按需加载)、商品列表页 URL 需要携带筛选和分页参数以便分享。
(1) 编程式导航:useNavigate
Tom 在登录页面需要处理表单提交,提交成功后根据用户角色跳转到不同页面。这种由代码逻辑触发的导航不能用 <Link>(Link 只能由用户点击触发),必须用 useNavigate。
useNavigate 返回一个 navigate 函数,支持三种调用方式:
| 调用方式 | 效果 | 示例 |
|---|---|---|
navigate('/path') |
跳转到指定路径(向历史栈添加新记录) | navigate('/dashboard') |
navigate('/path', { replace: true }) |
替换当前历史记录(不能后退到这里) | 登录后跳转 |
navigate(-1) |
后退 | 返回上一页 |
▶ 示例 1:登录后编程式导航
import { useNavigate, useLocation } from 'react-router-dom'
function LoginPage() {
const navigate = useNavigate()
const location = useLocation()
// 从 URL 查询参数中获取登录后要跳转的地址
const from = location.state?.from?.pathname || '/dashboard'
function handleLogin(event) {
event.preventDefault()
const formData = new FormData(event.target)
const username = formData.get('username')
// 模拟登录请求
fakeLogin(username).then(user => {
if (user.role === 'admin') {
navigate('/admin', { replace: true }) // 替换记录,不能后退到登录页
} else {
navigate(from, { replace: true }) // 跳转到登录前试图访问的页面
}
})
}
// 登录表单
return (
<form onSubmit={handleLogin}>
<input name="username" placeholder="用户名" required />
<button type="submit">登录</button>
<button type="button" onClick={() => navigate(-1)}>返回</button>
</form>
)
}
// 模拟登录 API
async function fakeLogin(username) {
await new Promise(r => setTimeout(r, 500))
return { name: username, role: username === 'admin' ? 'admin' : 'user' }
}
useLocation 的 role: location.state 可以从 Link 或 navigate 传递过来的状态数据。例如,在 ProtectedRoute 中重定向到登录页时,可以把用户原本要访问的页面路径存在 state 中,登录后自动跳转回去。
(2) 路由守卫:ProtectedRoute
| 守卫类型 | 检查逻辑 | 不通过时行为 | 典型场景 |
|---|---|---|---|
| 登录守卫 | isAuthenticated |
重定向到 /login |
仪表盘、个人中心 |
| 权限守卫 | user.role === 'admin' |
重定向到 /403 |
管理后台 |
| 功能开关守卫 | featureFlags.xEnabled |
重定向到 /upgrade |
付费功能 |
| 条件守卫 | profileComplete |
重定向到 /onboarding |
首次登录引导 |
Tom 的仪表盘页面需要登录才能访问,管理后台需要 admin 角色才能访问。他需要一种"守卫"机制——在渲染页面之前先检查用户状态,不满足条件就重定向。
路由守卫的本质是一个包装组件:它接收子组件,在渲染前执行检查逻辑。如果检查通过,渲染子组件;如果不通过,用 <Navigate> 重定向到其他页面。
▶ 示例 2:多层路由守卫
import { Navigate, useLocation } from 'react-router-dom'
// 模拟认证 Hook
function useAuth() {
return {
user: { name: 'Tom', role: 'admin' }, // 实际项目中从 Context 或 store 获取
isAuthenticated: true
}
}
// 第一层:登录守卫
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth()
const location = useLocation()
if (!isAuthenticated) {
// 把用户想去的位置存到 state 中,登录后跳转回来
return <Navigate to="/login" state={{ from: location }} replace />
}
return children
}
// 第二层:角色守卫
function AdminRoute({ children }) {
const { user } = useAuth()
if (user.role !== 'admin') {
return <Navigate to="/403" replace />
}
return children
}
// 路由配置中使用
function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/403" element={<AccessDenied />} />
{/* 需要登录 */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
{/* 需要登录 + admin 角色 */}
<Route path="/admin" element={
<ProtectedRoute>
<AdminRoute>
<AdminPanel />
</AdminRoute>
</ProtectedRoute>
} />
</Routes>
)
}
守卫嵌套模式: 外层 ProtectedRoute 检查"是否登录",内层 AdminRoute 检查"是否有 admin 权限"。职责分离,可复用。如果以后需要"编辑"角色,只需增加一个 EditorRoute 即可。
(3) 懒加载:React.lazy + Suspense
随着应用增长,Tom 发现首页加载时间越来越长——因为所有页面的代码都在一个 bundle 中,用户访问首页也需要下载管理后台的代码。React.lazy 允许将组件代码拆分成独立的 chunk,只在需要时才加载。
import { lazy, Suspense } from 'react'
// 这些组件不会被打包到主 bundle 中
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const AdminPanel = lazy(() => import('./pages/AdminPanel'))
const UserList = lazy(() => import('./pages/UserList'))
▶ 示例 3:路由级别的懒加载
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
// 懒加载所有页面组件
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const ProductList = lazy(() => import('./pages/ProductList'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Settings = lazy(() => import('./pages/Settings'))
const NotFound = lazy(() => import('./pages/NotFound'))
// 加载中组件
function PageLoader() {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
height: '100vh', fontSize: '1.2rem', color: '#666'
}}>
<div className="spinner" />
<span style={{ marginLeft: '12px' }}>页面加载中...</span>
</div>
)
}
function App() {
return (
<BrowserRouter>
{/* Suspense 包裹所有懒加载路由 */}
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
export default App
懒加载的工作原理: 当用户首次访问 /dashboard 时,React 才会动态加载 Dashboard.js 这个 chunk。加载期间,Suspense 的 fallback 组件会显示。加载完成后,替换成真正的 Dashboard 组件。
性能收益: 假设原 bundle 为 500KB,懒加载后主 bundle 降到 100KB,每个页面约 80KB。用户访问首页只需下载 100KB,而非 500KB——首屏加载速度提升约 5 倍。
4. useSearchParams:URL 查询参数管理
| URL 参数方式 | API | 语法示例 | 适用数据 |
|---|---|---|---|
| 路径参数 | useParams() |
/products/:id → { id: '42' } |
必需的标识符(资源 ID) |
| 查询参数 | useSearchParams() |
?sort=price&page=2 |
可选的筛选/排序/分页 |
| Hash | useLocation().hash |
#section-3 |
页内锚点定位 |
| State | useLocation().state |
navigate('/path', { state }) |
跨页面传递隐藏数据 |
Tom 的商品列表页需要支持分类筛选、价格排序、分页等功能,而且这些筛选条件要体现在 URL 中——这样用户可以把筛选后的链接分享给其他人。
(1) 查询参数的读写
useSearchParams 返回一个类 Map 的对象,可以像操作 Map 一样读写 URL 查询参数。它和 useState 类似,返回一个值和一个 setter。
import { useSearchParams } from 'react-router-dom'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
const category = searchParams.get('category') || '全部'
const sort = searchParams.get('sort') || 'default'
const page = Number(searchParams.get('page')) || 1
function updateFilter(key, value) {
setSearchParams(prev => {
if (value) prev.set(key, value)
else prev.delete(key)
return prev
})
}
return (
<div>
<p>当前分类:{category} | 排序:{sort} | 页码:{page}</p>
<button onClick={() => updateFilter('category', '电子')}>电子分类</button>
<button onClick={() => updateFilter('sort', 'price')}>按价格排序</button>
<button onClick={() => updateFilter('page', String(page + 1))}>下一页</button>
<button onClick={() => setSearchParams({})}>清除筛选</button>
</div>
)
}
// URL 会实时更新:/products?category=电子&sort=price&page=2
▶ 示例 4:完整的商品筛选功能
import { useSearchParams } from 'react-router-dom'
// 模拟商品数据
const allProducts = [
{ id: 1, name: 'iPhone 16', category: '电子', price: 6999 },
{ id: 2, name: 'React 编程书', category: '图书', price: 79 },
{ id: 3, name: '机械键盘', category: '电子', price: 399 },
{ id: 4, name: '设计模式入门', category: '图书', price: 59 },
{ id: 5, name: '蓝牙耳机', category: '电子', price: 899 },
]
function ProductListPage() {
const [searchParams, setSearchParams] = useSearchParams()
// 从 URL 读取筛选条件
const category = searchParams.get('category') || ''
const sortBy = searchParams.get('sort') || 'name'
const page = parseInt(searchParams.get('page') || '1', 10)
const pageSize = 3
// 筛选
let filtered = category
? allProducts.filter(p => p.category === category)
: allProducts
// 排序
if (sortBy === 'price') {
filtered = [...filtered].sort((a, b) => a.price - b.price)
} else {
filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name))
}
// 分页
const totalPages = Math.ceil(filtered.length / pageSize)
const paged = filtered.slice((page - 1) * pageSize, page * pageSize)
// 更新筛选条件
function setFilter(key, value) {
setSearchParams(prev => {
const next = new URLSearchParams(prev)
if (value) {
next.set(key, value)
} else {
next.delete(key)
}
next.set('page', '1') // 切换筛选时回到第一页
return next
})
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<label>分类:
<select value={category} onChange={e => setFilter('category', e.target.value)}>
<option value="">全部</option>
<option value="电子">电子</option>
<option value="图书">图书</option>
</select>
</label>
<label style={{ marginLeft: '16px' }}>排序:
<select value={sortBy} onChange={e => setFilter('sort', e.target.value)}>
<option value="name">名称</option>
<option value="price">价格</option>
</select>
</label>
</div>
<ul>
{paged.map(p => (
<li key={p.id}>{p.name} — ${p.price}({p.category})</li>
))}
</ul>
<div>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => setSearchParams(prev => {
const next = new URLSearchParams(prev)
next.set('page', String(i + 1))
return next
})}
style={{ fontWeight: page === i + 1 ? 'bold' : 'normal' }}
>
{i + 1}
</button>
))}
</div>
<p>当前 URL:/products?category={category}&sort={sortBy}&page={page}</p>
</div>
)
}
关键词: URL 查询参数是"可共享的状态"。用户筛选后复制 URL 发给同事,同事打开看到的筛选结果完全一致。这是用 useState 管理筛选条件做不到的。
5. 路由配置的组织策略
当项目路由增加到几十个时,把所有 Route 写在一个组件中会让代码难以维护。Tom 需要将路由配置抽取成独立模块。
(1) 路由配置文件
// src/routes/index.js
import { lazy } from 'react'
// 集中管理所有路由定义
const routes = [
{
path: '/',
component: lazy(() => import('../pages/Home')),
exact: true
},
{
path: '/login',
component: lazy(() => import('../pages/Login')),
},
{
path: '/dashboard',
component: lazy(() => import('../pages/Dashboard')),
protected: true // 需要登录
},
{
path: '/admin',
component: lazy(() => import('../pages/AdminPanel')),
protected: true,
adminOnly: true // 需要 admin 权限
},
{
path: '/products',
component: lazy(() => import('../pages/ProductList')),
},
{
path: '/products/:id',
component: lazy(() => import('../pages/ProductDetail')),
},
{
path: '*',
component: lazy(() => import('../pages/NotFound')),
}
]
export default routes
▶ 示例 5:路由渲染器
// src/routes/AppRouter.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { Suspense } from 'react'
import routes from './index'
import ProtectedRoute from '../components/ProtectedRoute'
import AdminRoute from '../components/AdminRoute'
function renderRoutes(routeList) {
return routeList.map(route => {
const Component = route.component
let element = <Component />
// 按需包裹守卫
if (route.protected) {
element = <ProtectedRoute>{element}</ProtectedRoute>
}
if (route.adminOnly) {
element = <AdminRoute>{element}</AdminRoute>
}
return (
<Route key={route.path} path={route.path} element={element} />
)
})
}
function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
{renderRoutes(routes)}
</Routes>
</Suspense>
</BrowserRouter>
)
}
export default AppRouter
这种模式的好处是:路由配置集中在一处,守卫逻辑自动包裹,新增页面只需在 routes/index.js 中添加一个对象即可。
(2) useLocation 监听路由变化
除了导航,Tom 还需要在某些场景下监听路由变化——例如页面路径变更时发送埋点数据、关闭弹窗、重置表单状态。useLocation 可以获取当前 URL 信息,配合 useEffect 监听路径变化。
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
function PageTracker() {
const location = useLocation()
useEffect(() => {
// 每次路径变化时触发
console.log('页面访问:', location.pathname + location.search)
// 埋点上报
analytics.pageView({
path: location.pathname,
search: location.search,
timestamp: Date.now()
})
}, [location]) // 依赖 location 对象
// 注意:location.pathname 或 location.search 任意一个变化都会触发
return null // 这个组件不渲染任何 UI
}
// 在 App 中使用
function App() {
return (
<BrowserRouter>
<PageTracker /> {/* 放在 Routes 外部,始终监听 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
)
}
useLocation 返回的关键属性: pathname(路径,如 /products/42)、search(查询字符串,如 ?category=电子)、hash(URL hash,如 #section-2)、state(通过 Link 或 navigate 传递的状态数据)。
❓ 常见问题
export default)的组件;(2) 必须在 Suspense 组件内部使用;(3) 不支持服务端渲染(SSR 需要用 @loadable/component 替代)。另外,懒加载组件加载期间如果网络慢,用户会看到 fallback 内容,建议 fallback 设计得小而快。📖 小节
- useNavigate 处理代码触发的导航(登录跳转、后退前进),
replace: true避免破坏历史栈 - 路由守卫通过包装组件实现,外层检查登录,内层检查权限,职责单一可复用
- React.lazy + Suspense 实现路由级别的代码分包,显著提升首屏加载速度
- useSearchParams 将筛选/分页状态同步到 URL,支持链接分享和浏览器前进后退
- 路由配置集中管理 + 自动渲染守卫,适合大型项目维护
📝 作业
- 实现一个登录页面:用户输入用户名后,用 useNavigate 跳转到仪表盘,并在跳转时用
replace: true防止后退到登录页。 - 用 ProtectedRoute 保护仪表盘路由,未登录时跳转到登录页,并在登录成功后自动跳转到用户原先想访问的地址。
- 用 useSearchParams 实现一个商品列表页:支持分类筛选、价格排序、分页,筛选条件反映在 URL 中,页面刷新后筛选条件不丢失。