React 路由
在现代单页面应用(SPA)中,路由是实现页面导航的核心机制。React Router 是 React 生态中最流行的路由库,它允许你在应用中实现声明式的路由导航,同时保持 UI 与 URL 的同步。
路由的基本概念
什么是客户端路由?
传统的多页面应用中,每次导航都会向服务器请求新页面。而单页面应用使用客户端路由,只加载一次页面,然后通过 JavaScript 动态更新内容,实现页面切换而不刷新浏览器。
传统多页面应用:
用户点击 → 请求服务器 → 返回新 HTML 页面 → 浏览器刷新
单页面应用:
用户点击 → JavaScript 处理 → 更新 UI + URL → 无刷新切换
React Router 的核心优势
- 声明式路由:像声明组件一样声明路由
- 嵌套路由:支持复杂的布局嵌套
- 代码分割:支持路由级别的懒加载
- 丰富的 API:提供多种 Hooks 和组件
安装与配置
安装
npm install react-router-dom
# 或
yarn add react-router-dom
# 或
pnpm add react-router-dom
基本配置
React Router 提供两种路由器组件:
BrowserRouter:使用 HTML5 History API(推荐用于生产)MemoryRouter:将 URL 存储在内存中(适合测试和演示)
实时编辑器
function BasicRouterDemo() { // 使用 MemoryRouter 进行演示 const Home = () => ( <div style={{ padding: '20px', background: '#f0f9ff', borderRadius: '8px' }}> <h2>首页</h2> <p>欢迎来到 React Router 教程!</p> </div> ); const About = () => ( <div style={{ padding: '20px', background: '#fef3c7', borderRadius: '8px' }}> <h2>关于我们</h2> <p>这是一个关于 React Router 的教程示例。</p> </div> ); const Contact = () => ( <div style={{ padding: '20px', background: '#dcfce7', borderRadius: '8px' }}> <h2>联系我们</h2> <p>邮箱: [email protected]</p> </div> ); return ( <MemoryRouter initialEntries={['/']}> <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px', padding: '10px', background: '#f8fafc', borderRadius: '8px' }}> <Link to="/" style={({ isActive }) => ({ padding: '8px 16px', background: isActive ? '#3b82f6' : '#e2e8f0', color: isActive ? 'white' : '#64748b', textDecoration: 'none', borderRadius: '4px' })} > 首页 </Link> <Link to="/about" style={{ padding: '8px 16px', background: '#e2e8f0', color: '#64748b', textDecoration: 'none', borderRadius: '4px' }}> 关于 </Link> <Link to="/contact" style={{ padding: '8px 16px', background: '#e2e8f0', color: '#64748b', textDecoration: 'none', borderRadius: '4px' }}> 联系 </Link> </nav> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> </Routes> </MemoryRouter> ); }
结果
Loading...
核心组件详解
BrowserRouter
BrowserRouter 是最常用的路由器,它使用 HTML5 History API 来保持 UI 与 URL 同步。
// 在应用入口使用
import { BrowserRouter } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
{/* 路由配置 */}
</Routes>
</BrowserRouter>
);
}
// 或者在 index.jsx 中包裹
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Routes 和 Route
Routes 是路由的容器,Route 定义单个路由规则。
<Routes>
{/* 精确匹配 */}
<Route path="/" element={<Home />} />
{/* 带参数的路由 */}
<Route path="/users/:id" element={<UserProfile />} />
{/* 嵌套路由 */}
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* 404 路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
Link 和 NavLink
Link 用于声明式导航,NavLink 是特殊的 Link,可以添加激活状态样式。
实时编辑器
function NavLinkDemo() { return ( <MemoryRouter initialEntries={['/']}> <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}> <NavLink to="/" style={({ isActive }) => ({ padding: '8px 16px', background: isActive ? '#3b82f6' : '#e2e8f0', color: isActive ? 'white' : '#64748b', textDecoration: 'none', borderRadius: '4px', fontWeight: isActive ? 'bold' : 'normal' })} > 首页 </NavLink> <NavLink to="/products" style={({ isActive }) => ({ padding: '8px 16px', background: isActive ? '#3b82f6' : '#e2e8f0', color: isActive ? 'white' : '#64748b', textDecoration: 'none', borderRadius: '4px', fontWeight: isActive ? 'bold' : 'normal' })} > 产品 </NavLink> <NavLink to="/about" style={({ isActive }) => ({ padding: '8px 16px', background: isActive ? '#3b82f6' : '#e2e8f0', color: isActive ? 'white' : '#64748b', textDecoration: 'none', borderRadius: '4px', fontWeight: isActive ? 'bold' : 'normal' })} > 关于 </NavLink> </nav> <Routes> <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff' }}>首页内容</div>} /> <Route path="/products" element={<div style={{ padding: '20px', background: '#fef3c7' }}>产品列表</div>} /> <Route path="/about" element={<div style={{ padding: '20px', background: '#dcfce7' }}>关于我们</div>} /> </Routes> </MemoryRouter> ); }
结果
Loading...
NavLink 还可以使用 className 属性:
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
关于
</NavLink>
路由 Hooks
useNavigate:编程式导航
useNavigate 返回一个函数,用于在代码中执行导航。
实时编辑器
function NavigateDemo() { const navigate = useNavigate(); const [path, setPath] = useState(''); return ( <MemoryRouter initialEntries={['/']}> <div style={{ marginBottom: '10px' }}> <input value={path} onChange={(e) => setPath(e.target.value)} placeholder="输入路径..." style={{ padding: '8px', marginRight: '10px' }} /> <button onClick={() => navigate(path || '/')} style={{ padding: '8px 16px' }}> 跳转 </button> </div> <div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}> <button onClick={() => navigate(-1)} style={{ padding: '8px 16px' }}> ← 后退 </button> <button onClick={() => navigate(1)} style={{ padding: '8px 16px' }}> 前进 → </button> <button onClick={() => navigate('/', { replace: true })} style={{ padding: '8px 16px' }}> 返回首页 </button> </div> <Routes> <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff' }}>首页 - 尝试输入路径跳转</div>} /> <Route path="/page1" element={<div style={{ padding: '20px', background: '#fef3c7' }}>页面 1</div>} /> <Route path="/page2" element={<div style={{ padding: '20px', background: '#dcfce7' }}>页面 2</div>} /> <Route path="*" element={<div style={{ padding: '20px', background: '#fee2e2' }}>404 - 页面不存在</div>} /> </Routes> </MemoryRouter> ); }
结果
Loading...
navigate 函数的用法:
const navigate = useNavigate();
// 导航到指定路径
navigate('/dashboard');
// 相对路径
navigate('../settings');
// 后退和前进
navigate(-1); // 后退一步
navigate(1); // 前进一步
navigate(-2); // 后退两步
// 替换当前历史记录
navigate('/login', { replace: true });
// 携带 state
navigate('/dashboard', { state: { from: 'login' } });
useParams:获取路由参数
useParams 用于获取 URL 中的动态参数。
实时编辑器
function ParamsDemo() { const UserPage = () => { const { id } = useParams(); return ( <div style={{ padding: '20px', background: '#f0f9ff', borderRadius: '8px' }}> <h3>用户详情</h3> <p>用户 ID: <strong style={{ color: '#3b82f6' }}>{id}</strong></p> </div> ); }; const ProductPage = () => { const { category, productId } = useParams(); return ( <div style={{ padding: '20px', background: '#fef3c7', borderRadius: '8px' }}> <h3>产品详情</h3> <p>分类: <strong>{category}</strong></p> <p>产品 ID: <strong style={{ color: '#d97706' }}>{productId}</strong></p> </div> ); }; return ( <MemoryRouter initialEntries={['/user/123']}> <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px', flexWrap: 'wrap' }}> <Link to="/user/123" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>用户 123</Link> <Link to="/user/456" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>用户 456</Link> <Link to="/products/electronics/laptop" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>电子产品/笔记本</Link> <Link to="/products/clothing/shirt" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>服装/衬衫</Link> </nav> <Routes> <Route path="/user/:id" element={<UserPage />} /> <Route path="/products/:category/:productId" element={<ProductPage />} /> </Routes> </MemoryRouter> ); }
结果
Loading...
useLocation:获取当前位置
useLocation 返回当前 URL 的位置信息对象。
实时编辑器
function LocationDemo() { const location = useLocation(); return ( <MemoryRouter initialEntries={['/']}> <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}> <Link to="/" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>首页</Link> <Link to="/about?ref=nav" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>关于 (带查询参数)</Link> <Link to="/contact#section1" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>联系 (带锚点)</Link> </nav> <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px', marginBottom: '15px' }}> <h4 style={{ margin: '0 0 10px' }}>Location 对象:</h4> <pre style={{ fontSize: '12px', margin: 0, overflow: 'auto' }}> {JSON.stringify({ pathname: location.pathname, search: location.search, hash: location.hash, state: location.state, key: location.key }, null, 2)} </pre> </div> <Routes> <Route path="/" element={<div style={{ padding: '20px' }}>首页</div>} /> <Route path="/about" element={<div style={{ padding: '20px' }}>关于页面</div>} /> <Route path="/contact" element={<div style={{ padding: '20px' }}>联系页面</div>} /> </Routes> </MemoryRouter> ); }
结果
Loading...
location 对象包含:
pathname:URL 路径search:查询字符串(如?ref=nav)hash:锚点(如#section1)state:导航时传递的状态key:唯一标识符
useSearchParams:处理查询参数
useSearchParams 用于读取和修改 URL 查询参数,类似 useState。
实时编辑器
function SearchParamsDemo() { const [searchParams, setSearchParams] = useSearchParams(); const page = searchParams.get('page') || '1'; const sort = searchParams.get('sort') || 'date'; const category = searchParams.get('category') || 'all'; const updateParams = (key, value) => { setSearchParams(prev => { prev.set(key, value); return prev; }); }; return ( <MemoryRouter initialEntries={['/?page=1&sort=date']}> <div style={{ marginBottom: '15px' }}> <label style={{ marginRight: '10px' }}> 页码: <select value={page} onChange={(e) => updateParams('page', e.target.value)} style={{ marginLeft: '5px', padding: '5px' }} > {[1, 2, 3, 4, 5].map(p => ( <option key={p} value={p}>{p}</option> ))} </select> </label> <label style={{ marginRight: '10px' }}> 排序: <select value={sort} onChange={(e) => updateParams('sort', e.target.value)} style={{ marginLeft: '5px', padding: '5px' }} > <option value="date">按日期</option> <option value="price">按价格</option> <option value="name">按名称</option> </select> </label> <label> 分类: <select value={category} onChange={(e) => updateParams('category', e.target.value)} style={{ marginLeft: '5px', padding: '5px' }} > <option value="all">全部</option> <option value="electronics">电子产品</option> <option value="clothing">服装</option> </select> </label> </div> <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px' }}> <p><strong>当前查询参数:</strong></p> <p>页码: {page}</p> <p>排序: {sort}</p> <p>分类: {category}</p> <p style={{ fontSize: '12px', color: '#64748b' }}> URL: ?{searchParams.toString()} </p> </div> </MemoryRouter> ); }
结果
Loading...
嵌套路由
嵌套路由允许你在父路由中渲染子路由,实现复杂的布局结构。
使用 Outlet
Outlet 是子路由的渲染占位符。
实时编辑器
function NestedRoutesDemo() { // 布局组件 const Layout = () => ( <div style={{ border: '2px solid #3b82f6', borderRadius: '8px', overflow: 'hidden' }}> <header style={{ padding: '15px', background: '#3b82f6', color: 'white' }}> <h2 style={{ margin: 0 }}>仪表盘</h2> </header> <div style={{ display: 'flex' }}> <nav style={{ width: '150px', padding: '10px', background: '#f1f5f9', minHeight: '200px' }}> <NavLink to="" end style={({ isActive }) => ({ display: 'block', padding: '8px', background: isActive ? '#dbeafe' : 'transparent', color: isActive ? '#1d4ed8' : '#64748b', textDecoration: 'none', borderRadius: '4px', marginBottom: '4px' })} > 概览 </NavLink> <NavLink to="stats" style={({ isActive }) => ({ display: 'block', padding: '8px', background: isActive ? '#dbeafe' : 'transparent', color: isActive ? '#1d4ed8' : '#64748b', textDecoration: 'none', borderRadius: '4px', marginBottom: '4px' })} > 统计 </NavLink> <NavLink to="settings" style={({ isActive }) => ({ display: 'block', padding: '8px', background: isActive ? '#dbeafe' : 'transparent', color: isActive ? '#1d4ed8' : '#64748b', textDecoration: 'none', borderRadius: '4px' })} > 设置 </NavLink> </nav> <main style={{ flex: 1, padding: '15px' }}> <Outlet /> </main> </div> </div> ); const Overview = () => <div> <h3>概览</h3> <p>欢迎回来!这是仪表盘概览页面。</p> </div>; const Stats = () => <div> <h3>统计</h3> <p>用户数: 1,234</p> <p>访问量: 56,789</p> </div>; const Settings = () => <div> <h3>设置</h3> <p>在这里配置您的偏好设置。</p> </div>; return ( <MemoryRouter initialEntries={['/dashboard']}> <Routes> <Route path="/dashboard" element={<Layout />}> <Route index element={<Overview />} /> <Route path="stats" element={<Stats />} /> <Route path="settings" element={<Settings />} /> </Route> </Routes> </MemoryRouter> ); }
结果
Loading...
index 路由
index 路由是父路径的默认子路由:
<Route path="/dashboard" element={<Layout />}>
{/* 访问 /dashboard 时显示 */}
<Route index element={<DashboardHome />} />
{/* 访问 /dashboard/settings 时显示 */}
<Route path="settings" element={<Settings />} />
</Route>
路由守卫(保护路由)
在实际应用中,某些页面需要用户登录后才能访问。我们可以创建一个保护路由组件。
实时编辑器
function ProtectedRouteDemo() { // 模拟登录状态 const [isLoggedIn, setIsLoggedIn] = useState(false); // 保护路由组件 const ProtectedRoute = ({ children }) => { const location = useLocation(); if (!isLoggedIn) { // 重定向到登录页,并保存原始路径 return <Navigate to="/login" state={{ from: location }} replace />; } return children; }; const LoginPage = () => { const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from?.pathname || '/'; const handleLogin = () => { setIsLoggedIn(true); navigate(from, { replace: true }); }; return ( <div style={{ padding: '20px', background: '#fee2e2', borderRadius: '8px' }}> <h3>请登录</h3> <p style={{ fontSize: '12px', color: '#64748b' }}> {location.state?.from ? `登录后将跳转到: ${location.state.from.pathname}` : ''} </p> <button onClick={handleLogin} style={{ padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 模拟登录 </button> </div> ); }; const DashboardPage = () => ( <div style={{ padding: '20px', background: '#dcfce7', borderRadius: '8px' }}> <h3>仪表盘 (受保护)</h3> <p>欢迎回来!这是受保护的页面。</p> <button onClick={() => setIsLoggedIn(false)} style={{ padding: '8px 16px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }} > 退出登录 </button> </div> ); return ( <MemoryRouter initialEntries={['/']}> <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px', alignItems: 'center' }}> <Link to="/" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>首页</Link> <Link to="/dashboard" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>仪表盘</Link> <span style={{ marginLeft: 'auto', fontSize: '12px', color: isLoggedIn ? '#22c55e' : '#ef4444' }}> {isLoggedIn ? '✓ 已登录' : '✗ 未登录'} </span> </nav> <Routes> <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff', borderRadius: '8px' }}>首页 - 公开页面</div>} /> <Route path="/login" element={<LoginPage />} /> <Route path="/dashboard" element={ <ProtectedRoute> <DashboardPage /> </ProtectedRoute> } /> </Routes> </MemoryRouter> ); }
结果
Loading...
懒加载路由
使用 React.lazy 和 Suspense 可以实现路由级别的代码分割,减少首屏加载时间。
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
404 页面处理
使用通配符路由捕获所有未匹配的路径。
实时编辑器
function NotFoundDemo() { return ( <MemoryRouter initialEntries={['/']}> <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}> <Link to="/" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>首页</Link> <Link to="/nonexistent" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>不存在的页面</Link> </nav> <Routes> <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff' }}>首页</div>} /> <Route path="*" element={ <div style={{ padding: '40px', background: '#fee2e2', borderRadius: '8px', textAlign: 'center' }}> <h1 style={{ margin: '0 0 10px', fontSize: '48px' }}>404</h1> <p style={{ margin: '0 0 15px', color: '#64748b' }}>页面不存在</p> <Link to="/" style={{ padding: '10px 20px', background: '#3b82f6', color: 'white', textDecoration: 'none', borderRadius: '4px' }} > 返回首页 </Link> </div> } /> </Routes> </MemoryRouter> ); }
结果
Loading...
路由配置最佳实践
集中式路由配置
对于大型应用,可以将路由配置集中管理:
// routes.js
const routes = [
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{
path: 'dashboard',
element: <ProtectedRoute><Dashboard /></ProtectedRoute>,
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'settings', element: <Settings /> }
]
},
{ path: '*', element: <NotFound /> }
]
}
];
// 使用递归渲染路由
function renderRoutes(routes) {
return routes.map((route, i) => (
<Route
key={i}
path={route.path}
element={route.element}
>
{route.children && renderRoutes(route.children)}
</Route>
));
}
滚动恢复
在导航时恢复滚动位置:
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
// 在应用中使用
<BrowserRouter>
<ScrollToTop />
<Routes>
{/* ... */}
</Routes>
</BrowserRouter>
小结
- BrowserRouter:使用 HTML5 History API 的路由器
- Routes/Route:声明式定义路由规则
- Link/NavLink:声明式导航,NavLink 支持激活状态
- useNavigate:编程式导航
- useParams:获取 URL 参数
- useLocation:获取当前位置信息
- useSearchParams:处理查询参数
- Outlet:嵌套路由的占位符
- 保护路由:实现权限控制
- 懒加载:代码分割优化性能
练习
- 实现一个带有登录保护的完整路由系统
- 创建一个多级嵌套的路由结构(如 /admin/users/:id/edit)
- 使用 useSearchParams 实现一个带筛选功能的列表页
- 实现路由级别的懒加载
- 创建一个面包屑导航组件,根据当前路由自动生成