Files
2026-01-25 15:05:03 +08:00

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