React: React Router 入门
最后更新:2026-08-26
Tom 正在开发一个电商管理后台,最初他用
useState来切换"页面"——首页、商品列表、订单详情各一个 state 变量。随着页面增加到 10 个,状态逻辑乱成一团,URL 地址栏永远停留在/,无法直接分享某个页面的链接。他意识到:缺少一个专业的路由方案。
1. 你将学到
- BrowserRouter vs HashRouter 的选择依据
- Routes 和 Route 的路径匹配机制
- Link 和 NavLink 声明式导航
- useParams 读取动态 URL 参数
- 嵌套路由与 Outlet 布局模式
2. 概念图解
flowchart LR
A[BrowserRouter<br/>路由容器] --> B[Routes<br/>路由表]
B --> C["Route path='/'<br/>→ Home"]
B --> D["Route path='/products'<br/>→ ProductList"]
B --> E["Route path='/products/:id'<br/>→ ProductDetail"]
B --> F["Route path='*'<br/>→ NotFound"]
C --> G[渲染组件]
D --> G
E --> G
F --> G
style A fill:#e1f5fe,stroke:#0288d1
style B fill:#fff3e0,stroke:#f57c00
style G fill:#e8f5e9,stroke:#388e3c
用户访问不同 URL → BrowserRouter 捕获 → Routes 匹配最合适的 Route → 渲染对应的组件。
3. 一个真实场景
Tom 的后台需要三个主页面:仪表盘、商品管理和系统设置。此外商品管理下还有商品列表和商品详情两个子页面。他希望每个页面都有独一无二的 URL,用户能通过浏览器的前进/后退按钮导航。
(1) 路由模式的选择
React Router 提供两种路由模式,它们的根本区别在于 URL 的处理方式:
| 模式 | URL 示例 | 原理 | 适用场景 |
|---|---|---|---|
BrowserRouter |
example.com/users |
利用 History API 操作 URL | 服务器能配置 URL 重写的项目 |
HashRouter |
example.com/#/users |
利用 URL hash 变化不触发服务器请求 | 静态托管(GitHub Pages、CDN) |
选择建议: 能配服务器的项目一律用 BrowserRouter——URL 更干净、SEO 更友好。静态托管用 HashRouter。
▶ 示例 1:基础路由配置
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function Home() {
return <h2>仪表盘主页</h2>
}
function ProductList() {
return <h2>商品列表</h2>
}
function Settings() {
return <h2>系统设置</h2>
}
function NotFound() {
return <h2>404 — 页面未找到</h2>
}
function App() {
return (
<BrowserRouter>
<nav style={{ display: 'flex', gap: '1rem', padding: '1rem', background: '#f0f0f0' }}>
<Link to="/">首页</Link>
<Link to="/products">商品管理</Link>
<Link to="/settings">系统设置</Link>
</nav>
<main style={{ padding: '1rem' }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</BrowserRouter>
)
}
export default App
执行效果: 点击导航链接,URL 随之变化,页面内容切换,但浏览器不会整页刷新——这就是 SPA 路由的核心体验。
关于 path="*": 通配符 * 匹配所有未在前面的 Route 中定义的路径,通常放在 Route 列表的末尾以实现 404 页面。在 React Router v6 中,* 只能作为 path 的末尾字符出现。
(2) NavLink 激活状态
| 导航组件 | 用途 | 激活样式 | 适用场景 |
|---|---|---|---|
<Link to="/path"> |
声明式跳转 | 无 | 通用导航链接 |
<NavLink to="/path"> |
带激活状态的导航 | isActive 回调 |
侧边栏/顶部导航高亮 |
navigate('/path') |
编程式跳转 | — | 登录后跳转、表单提交后跳转 |
<Navigate to="/path" /> |
声明式重定向 | — | 条件重定向组件 |
Tom 想在导航栏中高亮当前所在页面,让用户清楚地知道"我在哪里"。<NavLink> 组件提供了 isActive 参数,可以基于当前路由动态设置样式。它比 <Link> 多了两个额外属性:style 和 className 都支持接收一个带有 isActive 和 isPending 属性的回调函数。
▶ 示例 2:带激活样式的导航栏
import { NavLink } from 'react-router-dom'
function NavBar() {
const linkStyle = {
padding: '8px 16px',
textDecoration: 'none',
borderRadius: '6px',
transition: 'all 0.2s'
}
const activeStyle = {
...linkStyle,
backgroundColor: '#1976d2',
color: '#fff',
fontWeight: 'bold'
}
const inactiveStyle = {
...linkStyle,
color: '#333'
}
return (
<nav style={{ display: 'flex', gap: '12px', padding: '12px', background: '#fafafa' }}>
<NavLink
to="/"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
end // 精确匹配,避免 "/" 匹配所有以 "/" 开头的路径
>
首页
</NavLink>
<NavLink
to="/products"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
>
商品管理
</NavLink>
<NavLink
to="/settings"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
>
系统设置
</NavLink>
</nav>
)
}
关键点: end 属性让 / 路径只在完全匹配时激活,否则所有路径都会命中首页的激活样式。NavLink 的 className 也支持回调函数,适合使用 CSS 类名的项目。
(3) 嵌套路由与布局共享
Tom 发现商品管理页面内部还有"商品列表"和"添加商品"两个子页面,它们共享同一个侧边栏布局。如果每个子页面都重复写布局代码,既冗余又难以维护。React Router 的嵌套路由 + <Outlet> 完美解决这个问题。
嵌套路由的核心思想是父路由定义布局骨架,子路由通过 Outlet 注入内容。父组件不关心子路由具体渲染什么,只负责布局框架。
▶ 示例 3:嵌套路由与 Outlet
import { BrowserRouter, Routes, Route, Link, Outlet, useParams } from 'react-router-dom'
// 父布局组件 — 共享的侧边栏 + Outlet
function ProductsLayout() {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', padding: '16px', background: '#f5f5f5' }}>
<h3>商品管理</h3>
<nav style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Link to="list">商品列表</Link>
<Link to="add">添加商品</Link>
</nav>
</aside>
<main style={{ flex: 1, padding: '16px' }}>
{/* 子路由的组件在这里渲染 */}
<Outlet />
</main>
</div>
)
}
function ProductList() {
const products = [
{ id: 1, name: 'React 编程书', price: 79 },
{ id: 2, name: 'TypeScript 指南', price: 59 },
{ id: 3, name: 'Node.js 实战', price: 69 }
]
return (
<div>
<h2>商品列表</h2>
<ul>
{products.map(p => (
<li key={p.id}>
<Link to={`/products/detail/${p.id}`}>
{p.name} — ${p.price}
</Link>
</li>
))}
</ul>
</div>
)
}
function AddProduct() {
return (
<div>
<h2>添加商品</h2>
<form onSubmit={e => { e.preventDefault(); alert('提交成功!') }}>
<div><label>商品名:<input name="name" /></label></div>
<div><label>价格:<input name="price" type="number" /></label></div>
<button type="submit">提交</button>
</form>
</div>
)
}
function ProductDetail() {
const { id } = useParams()
return <h2>商品详情(ID:{id})</h2>
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<h2>首页</h2>} />
{/* 嵌套路由:父路由带布局,子路由通过 Outlet 渲染 */}
<Route path="/products" element={<ProductsLayout />}>
<Route index element={<ProductList />} /> {/* /products 默认显示 */}
<Route path="list" element={<ProductList />} /> {/* /products/list */}
<Route path="add" element={<AddProduct />} /> {/* /products/add */}
<Route path="detail/:id" element={<ProductDetail />} /> {/* /products/detail/1 */}
</Route>
<Route path="*" element={<h2>404 未找到</h2>} />
</Routes>
</BrowserRouter>
)
}
运行逻辑: 访问 /products/list → ProductsLayout 渲染侧边栏 → Outlet 位置显示 ProductList 组件。URL 结构与组件结构一一对应,清晰可维护。
关于 index 路由: index 路由是父路径的默认子路由。当访问 /products 时,没有匹配到任何子路径(list、add 都不匹配),此时 index 路由的内容会出现在 Outlet 中。它确保父路径不会显示空白区域。
4. 动态路由与路径参数
| 路径模式 | URL 示例 | useParams 返回值 | 说明 |
|---|---|---|---|
/users/:id |
/users/42 |
{ id: '42' } |
单个动态参数 |
/users/:userId/posts/:postId |
/users/42/posts/99 |
{ userId: '42', postId: '99' } |
多段动态参数 |
/files/* |
/files/a/b/c |
{ '*': 'a/b/c' } |
通配符匹配剩余路径 |
/categories/:catId/:tab? |
需定义两个 Route | { catId, tab } |
可选参数(无原生支持) |
动态路由是路由系统中最重要的功能之一。Tom 的商品详情页需要根据不同的商品 ID 展示不同的内容,他不可能为每个商品 ID 都写一个 Route——这就需要用 :param 语法定义动态路径。
(1) useParams 的使用
:id 是动态参数的占位符,实际的取值通过 useParams 这个 Hook 从 URL 中提取。
function UserDetail() {
const { userId, postId } = useParams()
return <p>用户 {userId} 的文章 {postId}</p>
}
// URL: /users/42/posts/99 → userId=42, postId=99
▶ 示例 4:商品详情页
import { useParams, Link, useNavigate } from 'react-router-dom'
// 模拟商品数据
const products = [
{ id: '1', name: 'React 编程书', price: 79, description: '从零到一掌握 React 18 开发' },
{ id: '2', name: 'TypeScript 指南', price: 59, description: '系统学习 TypeScript 类型系统' },
{ id: '3', name: 'Node.js 实战', price: 69, description: '后端开发从入门到进阶' }
]
function ProductDetail() {
const { id } = useParams()
const navigate = useNavigate()
const product = products.find(p => p.id === id)
if (!product) {
return (
<div>
<h2>商品不存在</h2>
<button onClick={() => navigate('/products')}>返回商品列表</button>
</div>
)
}
return (
<div>
<h2>{product.name}</h2>
<p className="price">价格:${product.price}</p>
<p className="desc">{product.description}</p>
<Link to="/products">← 返回列表</Link>
</div>
)
}
注意事项: 使用动态参数时需要考虑"参数值不存在对应数据"的情况。上述代码中的 if (!product) 分支就是空数据处理,避免访问不存在的 ID 时页面白屏。
(2) 多段动态参数
一个路径中可以包含多个动态参数,常见于嵌套资源的场景:
// 路由定义
<Route path="/categories/:catId/products/:prodId" element={<ProductView />} />
// 组件中提取
function ProductView() {
const { catId, prodId } = useParams()
// URL: /categories/electronics/products/42
// catId = "electronics", prodId = "42"
return <h2>分类 {catId} 下的商品 {prodId}</h2>
}
(3) 可选参数与通配符
React Router v6 不直接支持可选参数,但可以通过两种方式实现类似效果:
// 方式一:定义两个 Route(推荐)
<Route path="/categories/:catId" element={<CategoryPage />} />
<Route path="/categories/:catId/:tab" element={<CategoryPage />} />
// 方式二:在组件内自行判断
function CategoryPage() {
const { catId, tab } = useParams()
const activeTab = tab || 'overview' // 默认值
return <h2>分类 {catId} - {activeTab}</h2>
}
选择方式一的好处是 URL 语义清晰,方式二更简洁但降低了 URL 的可读性。
(4) 路径匹配优先级详解
React Router v6 的路径匹配基于评分算法,而非传统框架的"先匹配先服务"。理解这套规则有助于排查路由不生效的问题。
// 假设有以下路由配置
<Routes>
<Route path="/products/new" element={<NewProduct />} /> {/* 静态路径 */}
<Route path="/products/:id" element={<ProductDetail />} /> {/* 动态路径 */}
<Route path="/products/:id/edit" element={<EditProduct />} /> {/* 混合路径 */}
</Routes>
匹配优先级(从高到低):
- 静态路径段(
new)优先于动态参数段(:id) - 更多静态段的路径优先于更少静态段的路径
- 路径段数多的优先于段数少的
所以访问 /products/new 匹配 <NewProduct />,访问 /products/42 匹配 <ProductDetail />,访问 /products/42/edit 匹配 <EditProduct />。开发者无需担心顺序——系统会自动选择"最佳匹配"。
❓ 常见问题
/products/detail/1 这个具体路径,但服务器上没有这个文件。解决方案:在 nginx 中配置 try_files $uri $uri/ /index.html,将所有路由请求都指向 index.html,由 React Router 在前端做匹配。如果无法配置服务器,改用 HashRouter(# 后的部分不会发送到服务器)。<Link> 和原生 <a> 标签有什么区别?<a> 点击后会触发浏览器整页刷新,导致 SPA 丢失所有内存状态。<Link> 会阻止默认跳转,通过 History API 更新 URL 并通知 React Router 渲染新组件——不刷新页面、不丢失状态。SPA 中始终用 Link 或 NavLink 代替 <a>。/users/new 比 /users/:id 更具体,因此优先匹配。* 通配符匹配所有未匹配路径,通常放在最后用作 404 页面。路径默认是前缀匹配,添加 end 属性可改为精确匹配。<Route index element={...} /> 定义父路由的默认子路由。当访问父路由本身的路径时(如 /products),如果父路由使用了 Outlet,index 路由的内容会显示在 Outlet 位置。它相当于"父路径下的默认页",避免访问父路径时出现空白区域。<Routes> + <Route> 替代 <Switch>,Route 组件自动匹配最具体的路径;② 嵌套路由用 <Outlet> 替代手动渲染子路由;③ useNavigate() 替代 useHistory()——navigate('/path') 替代 history.push('/path'),navigate(-1) 替代 history.goBack()。v6 的 API 更简洁,但迁移需要改不少代码。(5) 相对路径 vs 绝对路径
在嵌套路由中,<Link to="..."> 的路径是相对于当前路由的,而 <Link to="/..."> 是绝对路径。理解这个区别对避免导航异常至关重要。
// 当前在 /products 下(ProductsLayout 组件内)
<Link to="list"> {/* → /products/list(相对路径,拼接到当前路由后) */}
<Link to="/list"> {/* → /list(绝对路径,直接替换) */}
<Link to="../settings"> {/* → /settings(父级相对路径) */}
在嵌套路由中,如果子路由在 ProductsLayout 中,内部的所有 Link 应该使用相对路径(不加前导 /),这样当父路由路径变化时,子路由的 Link 自动适配。
5. 404 页面与路由设计模式
(1) 通配符路由
path="*" 匹配所有未定义路径,是实现 404 页面的标准方式。但在 React Router v6 中,* 只能出现在路径末尾,不能像 path="/users/*/edit" 这样使用。
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/about" element={<About />} />
{/* 通配符路由必须放在最后 */}
<Route path="*" element={<NotFound />} />
</Routes>
)
}
function NotFound() {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<h1>404</h1>
<p>抱歉,您访问的页面不存在。</p>
<Link to="/">返回首页</Link>
</div>
)
}
(2) useNavigate 基础用法
虽然 useNavigate 在进阶课程中会详细讲解,但 Tom 在基础阶段也需要在某些场景下使用——例如登录页倒计时跳转、表单提交后跳转。
import { useNavigate } from 'react-router-dom'
function OrderSuccess() {
const navigate = useNavigate()
const [countdown, setCountdown] = useState(5)
useEffect(() => {
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(timer)
navigate('/orders') // 倒计时结束自动跳转
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [navigate])
return (
<div>
<h2>下单成功!</h2>
<p>{countdown} 秒后自动跳转到订单页</p>
<button onClick={() => navigate('/orders')}>立即查看</button>
</div>
)
}
▶ 示例 5:组合所有功能的管理后台
将本课学到的所有概念整合在一起——BrowserRouter、Routes、嵌套路由、NavLink、useParams、404 页面——构建一个完整的管理后台骨架。
import { BrowserRouter, Routes, Route, NavLink, Outlet, useParams } from 'react-router-dom'
import './App.css'
// 布局组件
function AdminLayout() {
return (
<div className="admin-container">
<header className="admin-header">
<h1>Tom 电商管理后台</h1>
</header>
<div className="admin-body">
<nav className="admin-sidebar">
<NavLink to="/" end>仪表盘</NavLink>
<NavLink to="/products">商品管理</NavLink>
<NavLink to="/orders">订单管理</NavLink>
<NavLink to="/settings">系统设置</NavLink>
</nav>
<main className="admin-content">
<Outlet />
</main>
</div>
</div>
)
}
// 页面组件
function Dashboard() {
return <h2>欢迎回来,Tom!今日订单数:42</h2>
}
function ProductsLayout() {
return (
<div>
<h2>商品管理</h2>
<nav>
<NavLink to="list">商品列表</NavLink>
<NavLink to="add">添加商品</NavLink>
</nav>
<Outlet />
</div>
)
}
function ProductList() {
return <p>这里显示商品列表...</p>
}
function AddProduct() {
return <p>这里显示添加商品表单...</p>
}
function Orders() {
return <h2>订单管理</h2>
}
function Settings() {
return <h2>系统设置</h2>
}
function NotFound() {
return <h2>404 — 页面不存在</h2>
}
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<AdminLayout />}>
<Route index element={<Dashboard />} />
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductList />} />
<Route path="list" element={<ProductList />} />
<Route path="add" element={<AddProduct />} />
</Route>
<Route path="orders" element={<Orders />} />
<Route path="settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
)
}
架构特点: 整个应用只有一个 <BrowserRouter>,一个顶级 <Routes>。AdminLayout 作为全局布局通过 Outlet 嵌套所有页面组件。这种"单路由 + 布局嵌套"的模式是中型 React 应用的标准架构。
📖 小节
- BrowserRouter 基于 History API,HashRouter 基于 URL hash,前者更推荐(需要服务器支持)
- Routes 组件负责匹配,Route 组件定义路径与组件的映射关系
- Link 做声明式导航,NavLink 提供激活状态检测(isActive 回调)
- useParams 从 URL 中提取动态参数,支持多段参数(
:id、:catId/:prodId) - 嵌套路由通过 Outlet 实现布局复用,index 路由提供父路径的默认内容
- 生产环境部署 BrowserRouter 必须配置服务器 URL 重写规则
- 嵌套路由中注意相对路径(无前导
/)和绝对路径(有前导/)的区别
📝 作业
- 创建一个 4 页面的管理后台:首页、用户列表、用户详情(通过 useParams 读取用户 ID)、关于页,使用 Link 进行导航,并确保 NavLink 有激活样式。
- 为用户列表页添加嵌套路由:
/users显示用户列表,/users/:id显示用户详情,两者共享一个带标题的布局组件(使用 Outlet)。 - 在路由表末尾添加一个 404 页面,当用户访问不存在的路径时显示友好的提示信息。