880 lines
19 KiB
Markdown
880 lines
19 KiB
Markdown
# 路由设计文档
|
|
|
|
## 1. 路由架构
|
|
|
|
### 1.1 路由结构
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ App Root │
|
|
├─────────────────────────────────────────┤
|
|
│ Router (React Router v6) │
|
|
│ ├─ /login (登录页) │
|
|
│ ├─ /403 (无权限页) │
|
|
│ ├─ /404 (404页) │
|
|
│ ├─ / (主布局) │
|
|
│ │ ├─ /dashboard (仪表盘) │
|
|
│ │ ├─ /projects (项目列表) │
|
|
│ │ ├─ /projects/create (创建项目) │
|
|
│ │ ├─ /projects/:id (项目详情) │
|
|
│ │ ├─ /projects/:id/edit (编辑项目) │
|
|
│ │ ├─ /users (用户列表) [admin] │
|
|
│ │ ├─ /users/create (创建用户) [admin] │
|
|
│ │ ├─ /users/:id (用户详情) [admin] │
|
|
│ │ └─ /statistics (统计分析) │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### 1.2 路由配置
|
|
|
|
```javascript
|
|
// router/routes.js
|
|
const routes = [
|
|
{
|
|
path: '/login',
|
|
element: <Login />,
|
|
meta: {
|
|
title: '登录',
|
|
requiresAuth: false,
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/403',
|
|
element: <Forbidden />,
|
|
meta: {
|
|
title: '无权限',
|
|
requiresAuth: false,
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/404',
|
|
element: <NotFound />,
|
|
meta: {
|
|
title: '页面不存在',
|
|
requiresAuth: false,
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/',
|
|
element: <Layout />,
|
|
meta: {
|
|
title: '主页',
|
|
requiresAuth: true,
|
|
},
|
|
children: [
|
|
{
|
|
path: '/dashboard',
|
|
element: <Dashboard />,
|
|
meta: {
|
|
title: '仪表盘',
|
|
icon: 'DashboardOutlined',
|
|
order: 1,
|
|
},
|
|
},
|
|
{
|
|
path: '/projects',
|
|
element: <ProjectList />,
|
|
meta: {
|
|
title: '项目管理',
|
|
icon: 'FileTextOutlined',
|
|
order: 2,
|
|
},
|
|
},
|
|
{
|
|
path: '/projects/create',
|
|
element: <ProjectForm />,
|
|
meta: {
|
|
title: '创建项目',
|
|
parent: '/projects',
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/projects/:id',
|
|
element: <ProjectDetail />,
|
|
meta: {
|
|
title: '项目详情',
|
|
parent: '/projects',
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/projects/:id/edit',
|
|
element: <ProjectForm />,
|
|
meta: {
|
|
title: '编辑项目',
|
|
parent: '/projects',
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/users',
|
|
element: <UserList />,
|
|
meta: {
|
|
title: '用户管理',
|
|
icon: 'UserOutlined',
|
|
order: 3,
|
|
requiresRole: ['admin'],
|
|
},
|
|
},
|
|
{
|
|
path: '/users/create',
|
|
element: <UserForm />,
|
|
meta: {
|
|
title: '创建用户',
|
|
parent: '/users',
|
|
requiresRole: ['admin'],
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/users/:id',
|
|
element: <UserDetail />,
|
|
meta: {
|
|
title: '用户详情',
|
|
parent: '/users',
|
|
requiresRole: ['admin'],
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/users/:id/edit',
|
|
element: <UserForm />,
|
|
meta: {
|
|
title: '编辑用户',
|
|
parent: '/users',
|
|
requiresRole: ['admin'],
|
|
hideInMenu: true,
|
|
},
|
|
},
|
|
{
|
|
path: '/statistics',
|
|
element: <Statistics />,
|
|
meta: {
|
|
title: '统计分析',
|
|
icon: 'BarChartOutlined',
|
|
order: 4,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export default routes;
|
|
```
|
|
|
|
## 2. 路由守卫
|
|
|
|
### 2.1 PrivateRoute组件
|
|
|
|
```javascript
|
|
// router/PrivateRoute.jsx
|
|
import { Navigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
/**
|
|
* 路由守卫组件
|
|
*/
|
|
const PrivateRoute = ({ children, meta = {} }) => {
|
|
const { isAuthenticated, user, loading } = useAuth();
|
|
const location = useLocation();
|
|
|
|
const { requiresAuth = true, requiresRole = [] } = meta;
|
|
|
|
// 检查加载状态
|
|
if (loading) {
|
|
return <Spin />;
|
|
}
|
|
|
|
// 检查认证状态
|
|
if (requiresAuth && !isAuthenticated) {
|
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
}
|
|
|
|
// 检查角色权限
|
|
if (requiresRole.length > 0 && !requiresRole.includes(user?.role)) {
|
|
return <Navigate to="/403" replace />;
|
|
}
|
|
|
|
return children;
|
|
};
|
|
|
|
export default PrivateRoute;
|
|
```
|
|
|
|
### 2.2 使用路由守卫
|
|
|
|
```javascript
|
|
// router/index.jsx
|
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
import PrivateRoute from './PrivateRoute';
|
|
import routes from './routes';
|
|
|
|
const Router = () => {
|
|
return (
|
|
<BrowserRouter>
|
|
<Routes>
|
|
{routes.map((route) => {
|
|
const { path, element, meta, children } = route;
|
|
|
|
if (children) {
|
|
return (
|
|
<Route
|
|
key={path}
|
|
path={path}
|
|
element={
|
|
<PrivateRoute meta={meta}>
|
|
{element}
|
|
</PrivateRoute>
|
|
}
|
|
>
|
|
{children.map((child) => (
|
|
<Route
|
|
key={child.path}
|
|
path={child.path}
|
|
element={
|
|
<PrivateRoute meta={child.meta}>
|
|
{child.element}
|
|
</PrivateRoute>
|
|
}
|
|
/>
|
|
))}
|
|
</Route>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Route
|
|
key={path}
|
|
path={path}
|
|
element={
|
|
<PrivateRoute meta={meta}>
|
|
{element}
|
|
</PrivateRoute>
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* 默认重定向 */}
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
|
|
{/* 404页面 */}
|
|
<Route path="*" element={<NotFound />} />
|
|
</Routes>
|
|
</BrowserRouter>
|
|
);
|
|
};
|
|
|
|
export default Router;
|
|
```
|
|
|
|
## 3. 路由参数处理
|
|
|
|
### 3.1 useParams Hook
|
|
|
|
```jsx
|
|
import { useParams } from 'react-router-dom';
|
|
|
|
const ProjectDetail = () => {
|
|
const { id } = useParams();
|
|
const { data, loading } = useApi(() => projectAPI.getDetail(id));
|
|
|
|
if (loading) return <Spin />;
|
|
return <div>{/* 项目详情 */}</div>;
|
|
};
|
|
```
|
|
|
|
### 3.2 useSearchParams Hook
|
|
|
|
```jsx
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { useDebounce } from '../hooks/useDebounce';
|
|
|
|
const ProjectList = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const keyword = searchParams.get('keyword') || '';
|
|
const debouncedKeyword = useDebounce(keyword, 300);
|
|
|
|
// 搜索逻辑
|
|
useEffect(() => {
|
|
fetchList({ keyword: debouncedKeyword });
|
|
}, [debouncedKeyword]);
|
|
|
|
const handleSearch = (value) => {
|
|
setSearchParams({ keyword: value });
|
|
};
|
|
|
|
return <Input.Search value={keyword} onChange={(e) => handleSearch(e.target.value)} />;
|
|
};
|
|
```
|
|
|
|
### 3.3 useLocation Hook
|
|
|
|
```jsx
|
|
import { useLocation } from 'react-router-dom';
|
|
|
|
const ProjectForm = () => {
|
|
const location = useLocation();
|
|
const isEditMode = location.pathname.includes('/edit');
|
|
|
|
return <div>{isEditMode ? '编辑模式' : '创建模式'}</div>;
|
|
};
|
|
```
|
|
|
|
### 3.4 useNavigate Hook
|
|
|
|
```jsx
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
const ProjectList = () => {
|
|
const navigate = useNavigate();
|
|
|
|
const handleCreate = () => {
|
|
navigate('/projects/create');
|
|
};
|
|
|
|
const handleEdit = (id) => {
|
|
navigate(`/projects/${id}/edit`);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
navigate(-1); // 返回上一页
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Button onClick={handleCreate}>创建项目</Button>
|
|
<Button onClick={() => handleEdit(1)}>编辑项目</Button>
|
|
<Button onClick={handleBack}>返回</Button>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
## 4. 路由元信息
|
|
|
|
### 4.1 路由元信息定义
|
|
|
|
```javascript
|
|
// router/routes.js
|
|
|
|
const routeMeta = {
|
|
// 页面标题
|
|
title: '项目管理',
|
|
|
|
// 菜单图标
|
|
icon: 'FileTextOutlined',
|
|
|
|
// 菜单排序
|
|
order: 1,
|
|
|
|
// 是否需要认证
|
|
requiresAuth: true,
|
|
|
|
// 允许的角色
|
|
requiresRole: ['admin', 'market'],
|
|
|
|
// 父路由
|
|
parent: '/projects',
|
|
|
|
// 是否在菜单中隐藏
|
|
hideInMenu: false,
|
|
|
|
// 是否在面包屑中隐藏
|
|
hideInBreadcrumb: false,
|
|
|
|
// 缓存配置
|
|
keepAlive: false,
|
|
};
|
|
```
|
|
|
|
### 4.2 使用路由元信息
|
|
|
|
```javascript
|
|
// hooks/useRouteMeta.js
|
|
import { useLocation, useParams } from 'react-router-dom';
|
|
import routes from '../router/routes';
|
|
|
|
/**
|
|
* 路由元信息Hook
|
|
*/
|
|
export const useRouteMeta = () => {
|
|
const location = useLocation();
|
|
const params = useParams();
|
|
|
|
// 查找当前路由配置
|
|
const findRouteMeta = (path, routeList) => {
|
|
for (const route of routeList) {
|
|
// 精确匹配
|
|
if (route.path === path) {
|
|
return route.meta;
|
|
}
|
|
|
|
// 参数匹配
|
|
const routePattern = route.path.replace(/:[^/]+/g, '[^/]+');
|
|
const regex = new RegExp(`^${routePattern}$`);
|
|
if (regex.test(path)) {
|
|
return route.meta;
|
|
}
|
|
|
|
// 递归查找子路由
|
|
if (route.children) {
|
|
const childMeta = findRouteMeta(path, route.children);
|
|
if (childMeta) return childMeta;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
const meta = findRouteMeta(location.pathname, routes);
|
|
|
|
return {
|
|
meta,
|
|
pathname: location.pathname,
|
|
search: location.search,
|
|
params,
|
|
};
|
|
};
|
|
```
|
|
|
|
### 4.3 动态设置页面标题
|
|
|
|
```javascript
|
|
// App.jsx
|
|
import { useEffect } from 'react';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { useRouteMeta } from './hooks/useRouteMeta';
|
|
|
|
const App = () => {
|
|
const { meta } = useRouteMeta();
|
|
|
|
useEffect(() => {
|
|
if (meta.title) {
|
|
document.title = `${meta.title} - 海洋项目管理系统`;
|
|
}
|
|
}, [meta.title]);
|
|
|
|
return <Router />;
|
|
};
|
|
```
|
|
|
|
## 5. 菜单生成
|
|
|
|
### 5.1 根据路由生成菜单
|
|
|
|
```javascript
|
|
// hooks/useMenu.js
|
|
import { useMemo } from 'react';
|
|
import { useAuth } from './useAuth';
|
|
import { useRouteMeta } from './useRouteMeta';
|
|
import routes from '../router/routes';
|
|
|
|
/**
|
|
* 生成菜单
|
|
*/
|
|
export const useMenu = () => {
|
|
const { hasRole } = useAuth();
|
|
const { pathname } = useRouteMeta();
|
|
|
|
const menuItems = useMemo(() => {
|
|
const generateMenu = (routeList) => {
|
|
return routeList
|
|
.filter((route) => {
|
|
// 过滤隐藏的菜单项
|
|
if (route.meta?.hideInMenu) return false;
|
|
|
|
// 过滤需要权限的菜单项
|
|
if (route.meta?.requiresRole?.length > 0) {
|
|
return hasRole(route.meta.requiresRole);
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.map((route) => {
|
|
if (route.children) {
|
|
return {
|
|
key: route.path,
|
|
icon: route.meta?.icon,
|
|
label: route.meta?.title,
|
|
children: generateMenu(route.children),
|
|
};
|
|
}
|
|
|
|
return {
|
|
key: route.path,
|
|
icon: route.meta?.icon,
|
|
label: route.meta?.title,
|
|
};
|
|
});
|
|
};
|
|
|
|
return generateMenu(routes);
|
|
}, [hasRole]);
|
|
|
|
const selectedKeys = useMemo(() => {
|
|
return [pathname];
|
|
}, [pathname]);
|
|
|
|
const openKeys = useMemo(() => {
|
|
// 根据当前路径展开子菜单
|
|
const paths = pathname.split('/').filter(Boolean);
|
|
const keys = paths.map((_, index) => `/${paths.slice(0, index + 1).join('/')}`);
|
|
return keys;
|
|
}, [pathname]);
|
|
|
|
return {
|
|
menuItems,
|
|
selectedKeys,
|
|
openKeys,
|
|
};
|
|
};
|
|
```
|
|
|
|
### 5.2 侧边栏菜单
|
|
|
|
```jsx
|
|
// components/Layout/Sidebar.jsx
|
|
import { Menu } from 'antd';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useMenu } from '../../hooks/useMenu';
|
|
|
|
const Sidebar = () => {
|
|
const navigate = useNavigate();
|
|
const { menuItems, selectedKeys, openKeys } = useMenu();
|
|
|
|
const handleMenuClick = ({ key }) => {
|
|
navigate(key);
|
|
};
|
|
|
|
return (
|
|
<div className="sidebar">
|
|
<div className="sidebar-logo">
|
|
<img src="/logo.png" alt="Logo" />
|
|
<span>海洋项目管理系统</span>
|
|
</div>
|
|
<Menu
|
|
mode="inline"
|
|
theme="dark"
|
|
items={menuItems}
|
|
selectedKeys={selectedKeys}
|
|
defaultOpenKeys={openKeys}
|
|
onClick={handleMenuClick}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
## 6. 面包屑导航
|
|
|
|
### 6.1 面包屑生成
|
|
|
|
```javascript
|
|
// hooks/useBreadcrumb.js
|
|
import { useMemo } from 'react';
|
|
import { useLocation, useMatches } from 'react-router-dom';
|
|
import routes from '../router/routes';
|
|
|
|
/**
|
|
* 生成面包屑
|
|
*/
|
|
export const useBreadcrumb = () => {
|
|
const location = useLocation();
|
|
|
|
const breadcrumbItems = useMemo(() => {
|
|
const items = [];
|
|
|
|
// 查找当前路径的所有路由
|
|
const pathSegments = location.pathname.split('/').filter(Boolean);
|
|
let currentPath = '';
|
|
|
|
for (const segment of pathSegments) {
|
|
currentPath += `/${segment}`;
|
|
|
|
// 查找路由配置
|
|
const findRoute = (routeList, path) => {
|
|
for (const route of routeList) {
|
|
if (route.path === path) {
|
|
return route;
|
|
}
|
|
|
|
if (route.children) {
|
|
const found = findRoute(route.children, path);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const route = findRoute(routes, currentPath);
|
|
|
|
if (route && !route.meta?.hideInBreadcrumb) {
|
|
items.push({
|
|
path: currentPath,
|
|
title: route.meta?.title || segment,
|
|
});
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}, [location.pathname]);
|
|
|
|
return { breadcrumbItems };
|
|
};
|
|
```
|
|
|
|
### 6.2 面包屑组件
|
|
|
|
```jsx
|
|
// components/Common/Breadcrumb.jsx
|
|
import { Breadcrumb } from 'antd';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useBreadcrumb } from '../../hooks/useBreadcrumb';
|
|
|
|
const BreadcrumbNav = () => {
|
|
const navigate = useNavigate();
|
|
const { breadcrumbItems } = useBreadcrumb();
|
|
|
|
const handleBreadcrumbClick = (path) => {
|
|
navigate(path);
|
|
};
|
|
|
|
return (
|
|
<Breadcrumb>
|
|
<Breadcrumb.Item onClick={() => navigate('/home')}>首页</Breadcrumb.Item>
|
|
{breadcrumbItems.map((item, index) => (
|
|
<Breadcrumb.Item
|
|
key={item.path}
|
|
onClick={() => handleBreadcrumbClick(item.path)}
|
|
>
|
|
{item.title}
|
|
</Breadcrumb.Item>
|
|
))}
|
|
</Breadcrumb>
|
|
);
|
|
};
|
|
```
|
|
|
|
## 7. 路由过渡动画
|
|
|
|
### 7.1 路由过渡组件
|
|
|
|
```jsx
|
|
// components/RouterTransition.jsx
|
|
import { useLocation } from 'react-router-dom';
|
|
import { CSSTransition, SwitchTransition } from 'react-transition-group';
|
|
import './RouterTransition.css';
|
|
|
|
const RouterTransition = ({ children }) => {
|
|
const location = useLocation();
|
|
|
|
return (
|
|
<SwitchTransition>
|
|
<CSSTransition
|
|
key={location.pathname}
|
|
timeout={300}
|
|
classNames="fade"
|
|
unmountOnExit
|
|
>
|
|
{children}
|
|
</CSSTransition>
|
|
</SwitchTransition>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 7.2 过渡动画样式
|
|
|
|
```css
|
|
/* RouterTransition.css */
|
|
.fade-enter {
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
}
|
|
|
|
.fade-enter-active {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
transition: opacity 300ms, transform 300ms;
|
|
}
|
|
|
|
.fade-exit {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.fade-exit-active {
|
|
opacity: 0;
|
|
transform: translateX(-20px);
|
|
transition: opacity 300ms, transform 300ms;
|
|
}
|
|
```
|
|
|
|
## 8. 路由缓存
|
|
|
|
### 8.1 KeepAlive组件
|
|
|
|
```jsx
|
|
// hooks/useKeepAlive.js
|
|
import { useState, useRef } from 'react';
|
|
|
|
const keepAliveCache = new Map();
|
|
|
|
export const useKeepAlive = (cacheKey, children) => {
|
|
const [isAlive, setIsAlive] = useState(false);
|
|
|
|
const activate = () => setIsAlive(true);
|
|
const deactivate = () => setIsAlive(false);
|
|
|
|
if (!keepAliveCache.has(cacheKey)) {
|
|
keepAliveCache.set(cacheKey, { children, isActive: false });
|
|
}
|
|
|
|
const cache = keepAliveCache.get(cacheKey);
|
|
|
|
return {
|
|
isAlive,
|
|
activate,
|
|
deactivate,
|
|
cachedChildren: cache.children,
|
|
};
|
|
};
|
|
```
|
|
|
|
### 8.2 使用KeepAlive
|
|
|
|
```jsx
|
|
const CachedComponent = () => {
|
|
const { cachedChildren } = useKeepAlive('dashboard', <Dashboard />);
|
|
return cachedChildren;
|
|
};
|
|
```
|
|
|
|
## 9. 路由权限控制
|
|
|
|
### 9.1 权限指令
|
|
|
|
```jsx
|
|
// components/Permission.jsx
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
/**
|
|
* 权限控制组件
|
|
*/
|
|
const Permission = ({ role, permission, children, fallback = null }) => {
|
|
const { user, hasRole, hasPermission } = useAuth();
|
|
|
|
// 检查角色权限
|
|
if (role && !hasRole(role)) {
|
|
return fallback;
|
|
}
|
|
|
|
// 检查功能权限
|
|
if (permission && !hasPermission(permission)) {
|
|
return fallback;
|
|
}
|
|
|
|
return children;
|
|
};
|
|
|
|
export default Permission;
|
|
```
|
|
|
|
### 9.2 使用权限组件
|
|
|
|
```jsx
|
|
import Permission from './Permission';
|
|
|
|
const ProjectList = () => {
|
|
return (
|
|
<div>
|
|
{/* 只有管理员可见 */}
|
|
<Permission role="admin">
|
|
<Button>删除项目</Button>
|
|
</Permission>
|
|
|
|
{/* 只有市场部用户可见 */}
|
|
<Permission role="market">
|
|
<Button>创建项目</Button>
|
|
</Permission>
|
|
|
|
{/* 没有权限时显示备用内容 */}
|
|
<Permission permission="edit_project" fallback={<span>无权限</span>}>
|
|
<Button>编辑项目</Button>
|
|
</Permission>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
## 10. 路由最佳实践
|
|
|
|
### 10.1 路由命名规范
|
|
|
|
```javascript
|
|
// 好的路由命名
|
|
'/projects' // 项目列表
|
|
'/projects/create' // 创建项目
|
|
'/projects/:id' // 项目详情
|
|
'/projects/:id/edit' // 编辑项目
|
|
|
|
// 不好的路由命名
|
|
'/p' // 太短,不清晰
|
|
'/project-list-page' // 太长,冗余
|
|
'/projects/1/edit' // 使用硬编码ID
|
|
```
|
|
|
|
### 10.2 路由嵌套
|
|
|
|
```javascript
|
|
// 合理的路由嵌套
|
|
<Route path="/projects" element={<Layout />}>
|
|
<Route index element={<ProjectList />} />
|
|
<Route path="create" element={<ProjectForm />} />
|
|
<Route path=":id" element={<ProjectDetail />} />
|
|
<Route path=":id/edit" element={<ProjectForm />} />
|
|
</Route>
|
|
|
|
// 不合理的路由嵌套(过深)
|
|
<Route path="/projects" element={<A />}>
|
|
<Route path="list" element={<B />}>
|
|
<Route path=":id" element={<C />}>
|
|
<Route path="detail" element={<D />} />
|
|
</Route>
|
|
</Route>
|
|
</Route>
|
|
```
|
|
|
|
### 10.3 路由懒加载
|
|
|
|
```javascript
|
|
// 懒加载路由组件
|
|
import { lazy, Suspense } from 'react';
|
|
|
|
const ProjectList = lazy(() => import('./pages/Projects/ProjectList'));
|
|
const ProjectDetail = lazy(() => import('./pages/Projects/ProjectDetail'));
|
|
|
|
const Router = () => {
|
|
return (
|
|
<Suspense fallback={<Spin />}>
|
|
<Routes>
|
|
<Route path="/projects" element={<ProjectList />} />
|
|
<Route path="/projects/:id" element={<ProjectDetail />} />
|
|
</Routes>
|
|
</Suspense>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
**文档维护**: 前端程序员
|
|
**文档类型**: 路由设计文档
|
|
**最后更新**: 2026-01-25
|