React Router
版本说明 : React Router V7 版本中,
react-router-dom
在 npm 上已经被推荐使用react-router
包。
目录
- [React Router 基础概念](#React Router 基础概念 "#1-react-router-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5")
- 路由配置基础
- 路由配置列表
- 路由通配符与重定向
- 路由加载逻辑处理
- 权限控制与异步加载
- 创建路由对象
- 页面组件实现
- 路由跳转与导航
- 数据加载与错误处理
- 完整示例
1. React Router 基础概念
什么是 React Router?
React Router 是 React 应用中最流行的路由库,它允许你构建单页应用(SPA)中的客户端路由。通过 React Router,你可以:
- 在 URL 变化时渲染不同的组件
- 实现浏览器历史记录管理
- 支持嵌套路由
- 处理路由参数和查询字符串
核心概念
1.1 路由(Route)
路由定义了 URL 路径与组件之间的映射关系。
1.2 路由器(Router)
路由器是路由系统的核心,负责监听 URL 变化并渲染对应的组件。
1.3 导航(Navigation)
导航允许用户在应用的不同页面之间跳转。
1.4 嵌套路由(Nested Routes)
允许在一个路由组件内部定义子路由,形成路由层次结构。
2. 路由配置基础
2.1 路由配置接口定义
首先,我们需要定义一个路由配置的接口,用于类型安全和代码提示:
tsx
import { redirect } from 'react-router';
// 路由配置接口
export interface RouterConfig {
name: string; // 路由名称,用于编程式导航
path: string; // 路由路径,支持动态参数
component?: any; // 路由对应的组件
children?: RouterConfig[]; // 子路由配置
meta?: { // 路由元信息
permission: string; // 权限标识
navName?: string; // 导航显示名称
title?: string; // 页面标题
icon?: string; // 图标
};
loader?: () => ReturnType<typeof redirect>; // 路由加载器
errorElement?: React.ComponentType; // 错误边界组件
}
2.2 路由路径语法
- 静态路径 :
/home
- 精确匹配 - 动态参数 :
/:id
- 匹配任意值,通过useParams()
获取 - 可选参数 :
/:id?
- 参数可选 - 通配符 :
*
- 匹配所有路径,通常用于 404 处理
3. 路由配置列表
3.1 基础路由配置示例
下面是一个典型的多层级路由配置示例,展示了如何组织复杂的应用路由结构:
tsx
import { redirect } from 'react-router';
import { lazy } from 'react';
// 路由页面配置列表,层级根据业务情况自定义
const routerConfig: RouterConfig[] = [
{
name: 'site',
path: '/:site/:lang', // 动态参数:site 和 lang
component: lazy(() => import('@/App')), // 懒加载根组件
children: [
{
name: 'main',
path: 'main', // 相对路径,完整路径为 /:site/:lang/main
component: lazy(() => import('@/pages/Main')),
children: [
{
name: 'home',
path: 'home', // 完整路径为 /:site/:lang/main/home
component: lazy(() => import('@/pages/Home')),
meta: {
permission: 'home', // 需要 home 权限
navName: '首页',
title: '首页'
},
children: [
{
name: 'lottie',
path: 'lottie', // 完整路径为 /:site/:lang/main/home/lottie
component: lazy(() => import('@/pages/Lottie')),
meta: {
permission: 'lottie',
navName: '动画页面',
title: 'Lottie 动画'
},
},
{
name: 'info',
path: 'info', // 完整路径为 /:site/:lang/main/home/info
component: lazy(() => import('@/pages/Info')),
meta: {
permission: 'info',
navName: '信息页面',
title: '信息详情'
},
}
]
}
]
},
{
name: 'login',
path: 'login', // 完整路径为 /:site/:lang/login
component: lazy(() => import('@/pages/Login')),
meta: {
title: '登录页面'
},
children: [], // 登录页面通常没有子路由
}
]
}
]
3.2 路由配置说明
- 动态参数 :
/:site/:lang
允许 URL 中包含动态的 site 和 lang 参数 - 懒加载 : 使用
lazy()
函数实现组件的按需加载,提升应用性能 - 嵌套路由 : 通过
children
属性实现路由的层级结构 - 权限控制 : 通过
meta.permission
字段控制页面访问权限 - 相对路径: 子路由的 path 是相对于父路由的路径
4. 路由通配符与重定向
4.1 通配符路由的作用
通配符路由(*
)用于处理所有未匹配的路由,通常用于:
- 404 页面处理
- 默认重定向
- 路由守卫和权限控制
4.2 重定向配置
在 routerConfig
数组的最后一项,添加 *
进行路由匹配重定向操作。当路由刷新、跳转操作时,匹配不到的路由地址,就会执行这里的 loader
函数:
tsx
{
name: 'redirect',
path: '*', // 通配符,匹配所有未定义的路由
loader: () => {
// 获取用户信息和状态
const site = sessionStorage.getItem('site')
const lang = sessionStorage.getItem('lang')
const token = sessionStorage.getItem('token')
// 根据登录状态进行重定向
if (!token) {
// 未登录用户重定向到登录页
return redirect(`/${site}/${lang}/login`)
}
// 已登录用户重定向到默认页面
return redirect(`/${site}/${lang}/main/home/lottie`)
},
children: []
}
4.3 重定向策略
可以根据不同的业务需求实现不同的重定向策略:
tsx
// 更复杂的重定向逻辑示例
{
name: 'redirect',
path: '*',
loader: () => {
const site = sessionStorage.getItem('site')
const lang = sessionStorage.getItem('lang')
const token = sessionStorage.getItem('token')
const userRole = sessionStorage.getItem('userRole')
const lastVisited = sessionStorage.getItem('lastVisited')
// 未登录用户
if (!token) {
return redirect(`/${site}/${lang}/login`)
}
// 根据用户角色重定向
if (userRole === 'admin') {
return redirect(`/${site}/${lang}/admin/dashboard`)
}
// 恢复用户上次访问的页面
if (lastVisited) {
return redirect(lastVisited)
}
// 默认重定向
return redirect(`/${site}/${lang}/main/home`)
},
children: []
}
5. 路由加载逻辑处理
5.1 路由配置转换
封装函数,将自定义的路由配置转换成 createBrowserRouter
方法所接受的参数类型:
tsx
import { createBrowserRouter, Outlet } from 'react-router';
/**
* MakeRoutes 函数:将自定义路由配置转换为 React Router 标准格式
* @param config 自定义路由配置数组
* @returns React Router 标准路由配置
*/
function MakeRoutes(config: RouterConfig[]): Parameters<typeof createBrowserRouter>[0] {
return config.map(({ path, component: Cmp, loader, children, meta }) => {
// 情况1:没有提供组件(通常是布局组件或中间层)
if (!Cmp) {
return {
path,
loader,
element: <Outlet />, // 渲染子路由的出口
children: [...MakeRoutes(children || [])], // 递归处理子路由
}
}
// 情况2:提供了组件,需要包装权限验证和异步加载
return {
path,
loader,
element: (
// 权限验证包装器
<PageRequireAuth permission={meta?.permission} key={String(Math.random())}>
{/* 异步加载包装器 */}
<SuspenseComponent>
{/* 实际的路由组件 */}
<Cmp />
</SuspenseComponent>
</PageRequireAuth>
),
children: [...MakeRoutes(children || [])], // 递归处理子路由
}
})
}
5.2 函数说明
- 递归处理 : 通过递归调用
MakeRoutes
处理嵌套路由 - 条件渲染 : 根据是否有组件来决定渲染
<Outlet />
还是实际组件 - 权限包装: 每个路由组件都会被权限验证组件包装
- 异步加载 : 使用
SuspenseComponent
处理懒加载组件的加载状态
6. 权限控制与异步加载
6.1 权限校验组件 (PageRequireAuth)
权限校验组件用于控制用户对特定页面的访问权限:
tsx
import { ReactNode } from 'react';
// 权限组件接收的参数
interface AuthProps {
permission?: string; // 需要的权限标识
children: ReactNode; // 子组件
}
/**
* 权限校验组件:根据用户权限控制页面渲染
* @param permission 需要的权限标识
* @param children 子组件
* @returns 有权限时渲染子组件,无权限时显示无权限页面
*/
export function PageRequireAuth({ permission, children }: AuthProps) {
// 从 sessionStorage 获取用户权限列表
const permissions = JSON.parse(sessionStorage.getItem('permissions') || '[]')
// 如果没有设置权限要求,直接渲染
if (!permission || !permissions) {
return <>{children}</>;
}
// 检查用户是否有对应权限
if (permissions.includes(permission)) {
return <>{children}</>;
}
// 无权限时显示无权限页面
return (
<div className="no-permission">
<h2>无权限访问</h2>
<p>您没有访问此页面的权限,请联系管理员。</p>
</div>
);
}
6.2 异步加载组件 (SuspenseComponent)
异步加载组件用于处理懒加载组件的加载状态:
tsx
import { Suspense, ReactNode } from 'react';
// 异步加载组件的参数
interface SuspenseProps {
children: ReactNode;
fallback?: ReactNode; // 自定义加载状态
}
/**
* 异步加载组件:为懒加载的组件提供加载状态
* @param children 懒加载的子组件
* @param fallback 加载时显示的组件
* @returns 包装了 Suspense 的组件
*/
export function SuspenseComponent({
children,
fallback = <div className="loading">Loading...</div>
}: SuspenseProps) {
return (
<Suspense fallback={fallback}>
{children}
</Suspense>
);
}
6.3 增强版权限控制
可以进一步扩展权限控制功能:
tsx
// 更完善的权限控制组件
export function PageRequireAuth({ permission, children }: AuthProps) {
const permissions = JSON.parse(sessionStorage.getItem('permissions') || '[]')
const userRole = sessionStorage.getItem('userRole')
// 管理员拥有所有权限
if (userRole === 'admin') {
return <>{children}</>;
}
// 检查具体权限
if (!permission || permissions.includes(permission)) {
return <>{children}</>;
}
// 可以重定向到无权限页面
return <Navigate to="/no-permission" replace />;
}
6.4 自定义加载状态
可以创建更丰富的加载状态组件:
tsx
// 自定义加载组件
const LoadingSpinner = () => (
<div className="loading-container">
<div className="spinner"></div>
<p>页面加载中...</p>
</div>
);
// 使用自定义加载状态
export function SuspenseComponent({ children }: SuspenseProps) {
return (
<Suspense fallback={<LoadingSpinner />}>
{children}
</Suspense>
);
}
7. 创建路由对象
7.1 路由创建函数
创建路由对象的核心函数,负责将配置转换为可用的路由器:
tsx
import { createBrowserRouter, RouterProvider } from 'react-router';
import { Suspense } from 'react';
/**
* 创建路由器的工厂函数
* @param routerConfig 路由配置数组
* @param basename 基础路径,会添加到所有路由前面
* @returns 配置好的路由器实例
*/
const createRouter = (routerConfig: RouterConfig[], basename = '') => {
// 将自定义配置转换为 React Router 标准格式
const config = MakeRoutes(routerConfig)
/*
* 这里可以添加额外的配置和中间件:
* - Sentry 错误上报
* - 路由守卫
* - 性能监控
* - 根据环境加载不同路由
*/
return createBrowserRouter(config, {
basename,
// 可以添加更多配置选项
future: {
v7_startTransition: true, // 启用 React 18 的 startTransition
v7_relativeSplatPath: true, // 改进相对路径处理
}
})
}
// 创建路由对象,basename 会添加到所有路由前面
export const router = createRouter(routerConfig, '/user')
// 导出路由渲染组件
export default function Routers() {
return (
<Suspense fallback={<div>应用加载中...</div>}>
<RouterProvider router={router} />
</Suspense>
)
}
7.2 路由配置选项
createBrowserRouter
支持多种配置选项:
tsx
const router = createBrowserRouter(config, {
basename: '/app', // 基础路径
future: {
v7_startTransition: true, // 使用 React 18 的并发特性
v7_relativeSplatPath: true, // 改进相对路径处理
},
// 全局错误处理
errorElement: <ErrorBoundary />,
// 全局加载器
loader: async () => {
// 全局数据加载逻辑
return null;
}
});
7.3 环境相关路由配置
可以根据不同环境创建不同的路由配置:
tsx
// 根据环境创建不同的路由配置
const createEnvironmentRouter = () => {
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
let config = routerConfig;
// 开发环境添加调试路由
if (isDevelopment) {
config = [
...config,
{
name: 'debug',
path: '/debug',
component: lazy(() => import('@/pages/Debug')),
meta: { title: '调试页面' }
}
];
}
// 生产环境移除调试功能
if (isProduction) {
config = config.filter(route => route.name !== 'debug');
}
return createRouter(config, '/user');
};
export const router = createEnvironmentRouter();
8. 页面组件实现
8.1 根组件 (App.tsx)
根组件是整个应用的入口,负责提供全局上下文和布局:
tsx
import { Outlet } from 'react-router'
import { Helmet, HelmetData } from 'react-helmet-async';
/**
* 页面标题组件
* 线程安全:确保在服务器端进行异步操作时,Helmet的状态能够针对每个请求独立维护,从而保证线程安全
* 状态封装:通过HelmetProvider组件为每个React组件树提供独立的状态,确保在服务器端渲染过程中安全地管理<head>标签中的内容
* SEO优化:动态设置页面标题
* 流式传输支持:支持流式渲染
*/
export function Title({ text }: { text: string }) {
const helmetData = new HelmetData({});
return (
<Helmet helmetData={helmetData}>
<title>{text}</title>
</Helmet>
);
}
/**
* 应用根组件
* 提供全局上下文、SEO支持和路由出口
*/
function App() {
return (
<>
<Title text="我的应用" />
{/*
创建全局上下文,用于:
- 用户信息管理
- 权限状态
- 主题配置
- 国际化设置
*/}
<AuthContext>
<Outlet />
</AuthContext>
</>
)
}
export default App
8.2 布局组件 (Main.tsx)
布局组件提供页面结构框架:
tsx
import { Outlet } from 'react-router';
import { ReactNode } from 'react';
/**
* 主布局组件
* 提供页面主体结构,包含导航、侧边栏等
*/
export default function Main() {
return (
<div className="main-layout">
{/* 页面头部 */}
<header className="main-header">
</header>
{/* 主要内容区域 */}
<main className="main-content">
<Outlet />
</main>
{/* 页面底部 */}
<footer className="main-footer">
</footer>
</div>
);
}
8.3 页面组件 (Home.tsx)
具体的页面组件实现:
tsx
import { FC } from 'react';
import { Outlet, useNavigate, useParams } from 'react-router';
/**
* 首页组件
* 展示主要功能入口和子页面导航
*/
const Home: FC = () => {
const navigate = useNavigate();
const params = useParams(); // 获取路由参数
// 退出登录
const logout = () => {
sessionStorage.removeItem('token');
sessionStorage.removeItem('permissions');
navigate('login');
};
// 导航到子页面
const navigateToPage = (page: string) => {
navigate(page);
};
return (
<div className="home-page">
<div className="home-header">
<h2>欢迎来到首页</h2>
<button onClick={logout} className="logout-btn">
退出登录
</button>
</div>
<div className="home-content">
<div className="navigation-buttons">
<button
onClick={() => navigateToPage('lottie')}
className="nav-btn"
>
Lottie 动画
</button>
<button
onClick={() => navigateToPage('info')}
className="nav-btn"
>
信息页面
</button>
</div>
{/* 子路由出口 */}
<div className="sub-content">
<Outlet />
</div>
</div>
</div>
);
};
export default Home;
9. 路由跳转与导航
9.1 useNavigate Hook
useNavigate
是 React Router 提供的 Hook,用于编程式导航:
tsx
import { useNavigate } from 'react-router';
const Home: FC = () => {
const navigate = useNavigate();
const logout = () => {
// 相对路径导航
navigate('login');
// 绝对路径导航
navigate('/user/site/zh/login');
// 带状态导航
navigate('/profile', {
state: { from: 'home' },
replace: true // 替换当前历史记录
});
// 后退导航
navigate(-1);
// 前进导航
navigate(1);
};
return <></>;
};
9.2 导航工具类
可以封装一个导航工具类,统一管理所有路由跳转:
tsx
import { useNavigate } from 'react-router';
/**
* 导航工具类
* 统一管理应用中的所有路由跳转
*/
class NavigationService {
private static navigate: ReturnType<typeof useNavigate> | null = null;
// 设置导航函数
static setNavigate(navigate: ReturnType<typeof useNavigate>) {
this.navigate = navigate;
}
// 跳转到登录页
static toLogin() {
this.navigate?.('/login');
}
// 跳转到首页
static toHome() {
this.navigate?.('/main/home');
}
// 跳转到指定页面
static toPage(path: string, options?: { replace?: boolean; state?: any }) {
this.navigate?.(path, options);
}
// 后退
static goBack() {
this.navigate?.(-1);
}
// 前进
static goForward() {
this.navigate?.(1);
}
}
// 在根组件中初始化
export function useNavigationService() {
const navigate = useNavigate();
NavigationService.setNavigate(navigate);
return NavigationService;
}
9.3 Link 组件
使用 Link
组件进行声明式导航:
tsx
import { Link, NavLink } from 'react-router';
// 基础 Link 组件
<Link to="/home">首页</Link>
// 带样式的 NavLink
<NavLink
to="/home"
className={({ isActive }) => isActive ? 'active' : ''}
>
首页
</NavLink>
// 带状态的导航
<Link
to="/profile"
state={{ from: 'home' }}
>
个人资料
</Link>
10. 数据加载与错误处理
10.1 Loader 数据加载
使用 loader
函数在路由渲染前加载数据:
tsx
// 用户数据加载器
async function userLoader({ params }: { params: any }) {
const userId = params.id;
const user = await fetchUser(userId);
if (!user) {
throw new Response("用户不存在", { status: 404 });
}
return { user };
}
// 在路由配置中使用
{
path: '/user/:id',
component: UserProfile,
loader: userLoader,
errorElement: <UserError />
}
10.2 错误边界处理
为路由提供错误处理:
tsx
import { useRouteError, isRouteErrorResponse } from 'react-router';
function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error-page">
<h1>{error.status}</h1>
<p>{error.statusText}</p>
</div>
);
}
return (
<div className="error-page">
<h1>出错了!</h1>
<p>发生了意外错误</p>
</div>
);
}
10.3 全局错误处理
在路由配置中添加全局错误处理:
tsx
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
errorElement: <ErrorBoundary />,
children: [
// 子路由...
]
}
]);
11. 完整示例
11.1 入口文件使用
tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import Routers from './routers'
createRoot(document.getElementById('root')!).render(
<StrictMode>
{/* 使用导出的路由渲染组件 */}
<Routers />
</StrictMode>
)
11.2 完整路由配置
tsx
import { createBrowserRouter, Outlet, redirect, RouterProvider } from 'react-router';
import { lazy, Suspense, FunctionComponent, ReactNode } from 'react';
export interface RouterConfig {
name: string;
path: string;
component?: any;
children?: RouterConfig[]
meta?: {
permission: string
navName?: string
title?: string
}
loader?: () => ReturnType<typeof redirect>
errorElement?: React.ComponentType;
}
// 路由页面配置列表
const routerConfig: RouterConfig[] = [
{
name: 'site',
path: '/:site/:lang',
component: lazy(() => import('@/App')),
children: [
{
name: 'main',
path: 'main',
component: lazy(() => import('@/pages/Main')),
children: [
{
name: 'home',
path: 'home',
component: lazy(() => import('@/pages/Home')),
meta: {
permission: 'home',
navName: '首页',
title: '首页'
},
children: [
{
name: 'lottie',
path: 'lottie',
component: lazy(() => import('@/pages/Lottie')),
meta: {
permission: 'lottie',
navName: '动画页面',
title: 'Lottie 动画'
},
},
{
name: 'info',
path: 'info',
component: lazy(() => import('@/pages/Info')),
meta: {
permission: 'info',
navName: '信息页面',
title: '信息详情'
},
}
]
}
]
},
{
name: 'login',
path: 'login',
component: lazy(() => import('@/pages/Login')),
meta: {
title: '登录页面'
},
children: [],
}
]
},
{
name: 'redirect',
path: '*',
loader: () => {
const site = sessionStorage.getItem('site')
const lang = sessionStorage.getItem('lang')
const token = sessionStorage.getItem('token')
if (!token) {
return redirect(`/${site}/${lang}/login`)
}
return redirect(`/${site}/${lang}/main/home/lottie`)
},
children: []
}
]
// 权限验证组件
interface AuthProps {
permission?: string
children: ReactNode
}
export function PageRequireAuth({ permission, children }: AuthProps) {
const permissions = JSON.parse(sessionStorage.getItem('permissions') || '[]')
if (!permission || !permissions) return (<>{children}</>);
if (permissions.includes(permission)) return (<>{children}</>);
return <div>无权限</div>;
}
// 异步加载组件
export function SuspenseComponent({ children }: { children: ReactNode }) {
return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
}
// 路由转换函数
function MakeRoutes(config: RouterConfig[]): Parameters<typeof createBrowserRouter>[0] {
return config.map(({ path, component: Cmp, loader, children, meta }) => {
if (!Cmp) return {
path,
element: <Outlet />,
children: [...MakeRoutes(children || [])],
loader
}
return {
path,
element: (
<PageRequireAuth permission={meta?.permission} key={String(Math.random())}>
<SuspenseComponent>
<Cmp />
</SuspenseComponent>
</PageRequireAuth>
),
children: [...MakeRoutes(children || [])],
loader
}
})
}
// 创建路由
const createRouter = (routerConfig: RouterConfig[], basename = '') => {
const config = MakeRoutes(routerConfig)
return createBrowserRouter(config, { basename })
}
export const router = createRouter(routerConfig, '/user')
// 路由渲染组件
export default function Routers() {
return (
<Suspense>
<RouterProvider router={router} />
</Suspense>
)
}