Router 动态路由
下面,我们来系统的梳理关于 Router 动态路由 的基本知识点:
一、动态路由概述
1.1 什么是动态路由
动态路由(Dynamic Routing)是React Router的核心特性之一,它允许我们创建基于URL参数的动态路径。与静态路由不同,动态路由可以根据URL中的变量值动态渲染不同内容。
关键特征:
- 路径中包含参数占位符(如
:id
,:slug
) - 根据URL参数变化渲染不同内容
- 支持嵌套路由结构
- 实现URL驱动的UI更新
1.2 为什么需要动态路由
- 内容动态化:展示不同资源(如用户资料、产品详情)
- 代码复用:使用同一组件处理相似数据
- SEO优化:为每个实体创建唯一URL
- 状态管理:URL作为应用状态的一部分
- 深度链接:允许直接访问特定内容
二、核心概念与API
2.1 路由参数(Route Parameters)
路由参数是路径中的占位符,用于捕获URL中的动态值:
<Route path="/users/:userId" element={<UserProfile />} />
:userId
是参数名称- 实际URL如
/users/123
或/users/mary
- 参数值通过
useParams()
钩子获取
2.2 通配符路由(Wildcard Routes)
使用 *
匹配任意路径段:
<Route path="/docs/*" element={<DocsLayout />} />
- 匹配
/docs/getting-started
和/docs/api/reference
- 剩余路径通过
useParams()['*']
获取
2.3 可选参数(Optional Parameters)
通过 ?
标记可选参数:
<Route path="/products/:category?/:productId" element={<ProductPage />} />
/products/laptops/123
→category: "laptops", productId: "123"
/products/456
→category: undefined, productId: "456"
2.4 正则表达式约束
在参数后添加正则表达式约束匹配格式:
<Route path="/user/:userId(\\d+)" element={<UserPage />} />
- 只匹配数字ID(如
/user/123
) - 不匹配非数字(如
/user/abc
)
三、实现动态路由
3.1 基础配置(v6版本)
import { BrowserRouter, Routes, Route } from 'react-router-dom';function App() {return (<BrowserRouter><Routes><Route path="/" element={<HomePage />} /><Route path="/users" element={<UserList />} /><Route path="/users/:userId" element={<UserProfile />} /><Route path="/products/:category/:productId" element={<ProductDetail />} /><Route path="/docs/*" element={<Documentation />} /><Route path="*" element={<NotFound />} /></Routes></BrowserRouter>);
}
3.2 获取路由参数
使用 useParams
钩子获取参数值:
import { useParams } from 'react-router-dom';function UserProfile() {const { userId } = useParams();// 示例:从API获取用户数据const [user, setUser] = useState(null);useEffect(() => {const fetchUser = async () => {const response = await fetch(`/api/users/${userId}`);const data = await response.json();setUser(data);};fetchUser();}, [userId]);if (!user) return <div>Loading...</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Member since: {new Date(user.joinDate).toLocaleDateString()}</p></div>);
}
3.3 获取查询参数
使用 useSearchParams
处理查询字符串:
import { useSearchParams } from 'react-router-dom';function SearchResults() {const [searchParams, setSearchParams] = useSearchParams();const query = searchParams.get('q');const page = searchParams.get('page') || 1;// 更新查询参数const updatePage = (newPage) => {setSearchParams({ q: query, page: newPage });};return (<div><h2>Search results for: {query}</h2>{/* 显示结果 */}<div><button onClick={() => updatePage(Number(page) - 1)}>Previous</button><span>Page {page}</span><button onClick={() => updatePage(Number(page) + 1)}>Next</button></div></div>);
}
3.4 嵌套动态路由
<Routes><Route path="/projects" element={<ProjectLayout />}><Route index element={<ProjectList />} /><Route path=":projectId" element={<ProjectDetails />}><Route index element={<ProjectOverview />} /><Route path="settings" element={<ProjectSettings />} /><Route path="team" element={<ProjectTeam />} /></Route></Route>
</Routes>
四、技巧与实践
4.1 数据加载模式
1. 加载器函数(Loader Functions)
使用React Router的loader在路由匹配时加载数据:
import { createBrowserRouter, RouterProvider } from 'react-router-dom';const router = createBrowserRouter([{path: '/users/:userId',element: <UserProfile />,loader: async ({ params }) => {return fetch(`/api/users/${params.userId}`);}}
]);function App() {return <RouterProvider router={router} />;
}// 在组件中使用useLoaderData获取数据
import { useLoaderData } from 'react-router-dom';function UserProfile() {const user = useLoaderData();// 渲染用户数据...
}
2. Suspense集成
<Suspense fallback={<Spinner />}><Routes><Route path="/blog/:slug" element={<BlogPost />} /></Routes>
</Suspense>
4.2 编程式导航
import { useNavigate, useParams } from 'react-router-dom';function EditUserButton() {const { userId } = useParams();const navigate = useNavigate();const handleEdit = () => {// 导航到编辑页面navigate(`/users/${userId}/edit`);// 或者替换当前历史记录// navigate(`/users/${userId}/edit`, { replace: true });};return (<button onClick={handleEdit}>Edit Profile</button>);
}
4.3 路由守卫与权限控制
// 高阶组件保护路由
function ProtectedRoute({ children }) {const { isAuthenticated } = useAuth();const location = useLocation();if (!isAuthenticated) {return <Navigate to="/login" state={{ from: location }} replace />;}return children;
}// 路由配置中使用
<Route path="/dashboard/:section" element={<ProtectedRoute><Dashboard /></ProtectedRoute>}
/>
4.4 动态路由生成
基于数据生成路由:
function DynamicRoutes() {const [categories, setCategories] = useState([]);useEffect(() => {fetch('/api/categories').then(res => res.json()).then(data => setCategories(data));}, []);return (<Routes>{categories.map(category => (<Route key={category.id} path={`/shop/${category.slug}`} element={<CategoryPage />} />))}</Routes>);
}
五、常见问题解决方案
5.1 参数变化检测
当路由参数变化但组件未更新时:
function ProductPage() {const { productId } = useParams();useEffect(() => {// 当productId变化时重新获取数据fetchProduct(productId);}, [productId]); // ✅ 添加参数依赖// ...
}
5.2 滚动恢复
<BrowserRouter><ScrollRestoration /><Routes>{/* 路由配置 */}</Routes>
</BrowserRouter>
自定义滚动行为:
const scrollRestoration = {top: 0,left: 0,behavior: 'auto'
};<BrowserRouterwindow={window}scrollRestoration={scrollRestoration}
>{/* 应用内容 */}
</BrowserRouter>
5.3 404处理
全局404:
<Route path="*" element={<NotFoundPage />} />
嵌套路由中的404:
<Route path="/dashboard" element={<DashboardLayout />}><Route index element={<DashboardHome />} /><Route path="settings" element={<Settings />} /><Route path="*" element={<DashboardNotFound />} />
</Route>
六、示例:电商产品路由
// App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';function App() {return (<BrowserRouter><Header /><Routes><Route path="/" element={<HomePage />} /><Route path="/products" element={<ProductListPage />} /><Route path="/products/:category" element={<CategoryPage />} /><Route path="/product/:productId" element={<ProductDetailPage />} /><Route path="/cart" element={<CartPage />} /><Route path="/checkout" element={<CheckoutPage />} /><Route path="*" element={<NotFoundPage />} /></Routes><Footer /></BrowserRouter>);
}// ProductDetailPage.js
import { useParams, useNavigate, Link } from 'react-router-dom';function ProductDetailPage() {const { productId } = useParams();const [product, setProduct] = useState(null);const [loading, setLoading] = useState(true);const navigate = useNavigate();useEffect(() => {const fetchProduct = async () => {try {const response = await fetch(`/api/products/${productId}`);const data = await response.json();setProduct(data);} catch (error) {console.error('Failed to fetch product:', error);navigate('/not-found', { replace: true });} finally {setLoading(false);}};fetchProduct();}, [productId, navigate]);if (loading) return <LoadingSpinner />;return (<div className="product-detail"><div className="breadcrumb"><Link to="/">Home</Link> > <Link to={`/products/${product.category}`}>{product.categoryName}</Link> > <span>{product.name}</span></div><div className="product-content"><div className="product-images"><img src={product.mainImage} alt={product.name} /></div><div className="product-info"><h1>{product.name}</h1><p className="price">${product.price.toFixed(2)}</p><p>{product.description}</p><div className="actions"><AddToCartButton productId={product.id} /><button onClick={() => navigate(-1)} className="back-button">Back to Previous</button></div></div></div><RelatedProducts category={product.category} /></div>);
}
七、总结与实践
7.1 动态路由最佳实践
- 命名一致性:使用一致的参数命名(如
:id
,:slug
) - 参数验证:验证参数格式和有效性
- 错误处理:处理无效参数和加载错误
- 代码分割:使用React.lazy实现路由级代码分割
- SEO优化:为动态页面添加合适的元标签
- URL设计:创建用户友好的URL结构
- 状态管理:避免在URL中存储敏感数据
7.2 性能优化
- 数据预取:使用loader函数在渲染前获取数据
- 缓存策略:实现数据缓存减少API调用
- 懒加载:动态导入大型路由组件
- 虚拟化列表:对长列表使用虚拟滚动
7.3 测试策略
- 单元测试:测试参数解析和组件逻辑
- 集成测试:测试路由导航和数据加载
- 端到端测试:测试完整用户流程
// 使用React Testing Library测试路由
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import UserProfile from './UserProfile';test('displays user profile for given ID', () => {render(<BrowserRouter><Routes><Route path="/users/:userId" element={<UserProfile />} /></Routes></BrowserRouter>,{ initialEntries: ['/users/123'] });expect(screen.getByText('Loading...')).toBeInTheDocument();await waitFor(() => {expect(screen.getByText('John Doe')).toBeInTheDocument();expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();});
});