19 KiB
19 KiB
路由设计文档
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 路由配置
// 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组件
// 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 使用路由守卫
// 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
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
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
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
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 路由元信息定义
// 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 使用路由元信息
// 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 动态设置页面标题
// 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 根据路由生成菜单
// 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 侧边栏菜单
// 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 面包屑生成
// 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 面包屑组件
// 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 路由过渡组件
// 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 过渡动画样式
/* 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组件
// 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
const CachedComponent = () => {
const { cachedChildren } = useKeepAlive('dashboard', <Dashboard />);
return cachedChildren;
};
9. 路由权限控制
9.1 权限指令
// 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 使用权限组件
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 路由命名规范
// 好的路由命名
'/projects' // 项目列表
'/projects/create' // 创建项目
'/projects/:id' // 项目详情
'/projects/:id/edit' // 编辑项目
// 不好的路由命名
'/p' // 太短,不清晰
'/project-list-page' // 太长,冗余
'/projects/1/edit' // 使用硬编码ID
10.2 路由嵌套
// 合理的路由嵌套
<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 路由懒加载
// 懒加载路由组件
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