React Router 核心功能
以下集成内容对React Router 的核心概念和基础用法,包括路由配置、Outlet、路由跳转、懒加载等进行说明,如需进行代码对照,可参考目录下 react19_ts 项目内实际代码,文档与代码会在git上持续补充
git仓库地址:https://gitee.com/xiaoli-account/react19_ts> 本项目使用 `react-router-dom@^7.12.0`,基于 `BrowserRouter` 模式,采用**集中式路由配置 + 数据驱动渲染**的架构方案。
> 本文档介绍 React Router 的核心概念和基础用法,包括路由配置、Outlet、路由跳转、懒加载等。
> 内容较多,建议耐心阅读
目录
1. 项目路由架构总览2. 路由配置 routes.ts 2.1 RouteItem 类型定义 2.2 RouteMeta 类型定义 2.3 路由分层设计 2.4 添加新路由3. 路由渲染组件 Outlet 3.1 布局嵌套与 Outlet4. 路由版本演进 4.1 LeeRouter V1 — 静态硬编码 4.2 LeeRouter V2 — 数据驱动渲染 4.3 LeeRouter V3 — 后端接口控制(当前版本)5. 路由跳转 5.1 普通页面跳转 5.2 带参页面跳转 5.3 非组件环境跳转 — NavigationService 降级方案6. 懒加载 & Suspense7. 关键文件索引
1. 项目路由架构总览
src/App.tsx ← 应用入口,挂载 BrowserRouter └─ src/router/index.tsx ← AppRouter 入口,向 LeeRouter 注入路由配置 └─ src/layout/router/index.tsx ← LeeRouter(V1/V2/V3),路由引擎核心 ├─ route-utils.tsx ← V2 渲染工具:递归 <Route> + Suspense ├─ route-utils.ts ← V3 渲染工具:transformToRouteObject → useRoutes └─ router-type.ts ← 类型定义:RouteItem / RouteMeta / MenuItemsrc/router/routes.ts ← 业务路由配置(静态/布局/动态)src/layout/utils/navigation.ts ← NavigationService:非组件环境跳转
请求链路:
App.tsx → BrowserRouter → AppRouter → LeeRouterV3 → 初始化 NavigationService → 初始化 LeeLogger → 通过 useRouterStore.getRoutes() 获取路由 → 页面刷新时从 useMenuStore 持久化数据恢复路由 + 重新初始化权限 → transformToRouteObject() 转换为 RouteObject[] → useRoutes(routeConfig) 渲染
2. 路由配置 routes.ts
路由配置文件:`src/router/routes.ts`
2.1 RouteItem 类型定义
类型文件:`src/layout/router/router-type.ts`
export interface RouteItem { path: string; // 路由路径 component?: ComponentType | (() => Promise<{ default: ComponentType }>); // 组件或懒加载函数 children?: RouteItem[]; // 子路由 redirect?: string; // 重定向目标 meta?: RouteMeta; // 元信息 name?: string; // 路由名称(用作缓存 key)}
约定:本项目的 `component` 字段统一使用 `() => import('...')` 懒加载写法,框架内部通过 `React.lazy()` + `Suspense` 自动处理。
2.2 RouteMeta 类型定义
export interface RouteMeta { title: string; // 页面标题,用于菜单/面包屑/Tab 标签的显示名称 icon?: string; // Ant Design 图标名(如 "HomeOutlined"),用于菜单图标 requiresAuth?: boolean; // 是否需要鉴权,true 时检查 pagePermission pagePermission?: string; // 页面权限标识,格式:page:module:action(如 "page:system:user") hidden?: boolean; // true 时在侧边栏菜单中隐藏 keepAlive?: boolean; // true 时启用页签 KeepAlive 缓存 order?: number; // 菜单排序(数字越小越靠前) external?: string; // 外链 URL target?: "_blank" | "_self"; // 外链打开方式 i18n?: string; // 国际化 key,格式:lee-layout-routes.{RouteName}}
2.3 路由分层设计
项目路由分为三层,最终合并导出:
// ① 静态路由 — 无需权限控制,如登录页、错误页export const staticRoutes: RouteItem[] = [ { path: "/", redirect: "/loading" }, { path: "/login", name: "Login", component: () => import("@/pages/login"), meta: { requiresAuth: false, hidden: true } }, { path: "/401", name: "Error401", component: () => import("@/pages/error/401"), meta: { requiresAuth: false, hidden: true } }, // ... /403, /404, /500, /502, /503, /504];// ② 布局路由 — 挂载 Layout 组件,所有需要侧边栏/头部/页签的页面放在 children 中export let layoutRoutes: RouteItem = { path: "/", name: "Layout", component: () => import("@/layout"), // 布局管理器 LayoutManager redirect: "/dashboard", // 默认重定向首页 children: [], // 动态填充};// ③ 配置路由 — 需要权限控制的业务页面export const configRoutes: RouteItem[] = [ { path: "dashboard", // 注意:子路由不需要 "/" 前缀 name: "Dashboard", component: () => import("@/pages/dashboard"), meta: { title: "首页", icon: "HomeOutlined", keepAlive: true, requiresAuth: true }, }, { path: "system-management", name: "SystemManagement", meta: { title: "系统管理", icon: "SettingOutlined" }, // 目录节点,无 component children: [ { path: "user-management", name: "UserManagement", component: () => import("@/pages/system-management/user-management"), ... }, { path: "role-management", name: "RoleManagement", component: () => import("@/pages/system-management/role-management"), ... }, // ... ], }, // ...];// ④ 合并导出 — 将配置路由填入布局路由的 childrenexport const asyncRoutes = () => { layoutRoutes.children = [...configRoutes]; return layoutRoutes;};export default [...staticRoutes, asyncRoutes()];
路由层级关系:
/ → redirect → /loading/loading → SSO 加载页/login → 登录页/ → LayoutManager(侧边栏 + 头部 + 页签) ├─ /dashboard → 首页(index 重定向) ├─ /system-management │ ├─ /user-management → 用户管理 │ ├─ /role-management → 角色管理 │ ├─ /menu-management → 菜单管理 │ └─ ... ├─ /examples │ ├─ /basic-example → 基础页面示例 │ ├─ /ajax-example → Ajax 示例 │ └─ ... └─ /profile → 个人中心(hidden: true)/401 /403 /404 /500 ... → 错误页* → 404 兜底
2.4 添加新路由
在 `src/router/routes.ts` 的 `configRoutes` 数组中添加即可,框架会自动处理懒加载、菜单渲染和权限过滤:
// 在 configRoutes 中添加{ path: "my-new-page", name: "MyNewPage", component: () => import("@/pages/my-new-page"), meta: { title: "我的新页面", i18n: "lee-layout-routes.MyNewPage", icon: "AppstoreOutlined", pagePermission: "page:my-new-page", requiresAuth: true, keepAlive: true, // 启用 Tab 缓存 hidden: false, // 在菜单中显示 },},
⚠️注意:新增页面的 `pagePermission` 需要在权限白名单或后端权限列表中配置,否则页面会被过滤。
3. 路由渲染组件 Outlet
3.1 布局嵌套与 Outlet
本项目的核心嵌套关系是:**Layout 组件包裹 `<Outlet />`**,子路由的内容渲染到 `<Outlet />` 位置。
┌───────────────────────────────────────────┐│ Layout(Header + Sidebar + Tabs) ││ ┌────────────────────────────────────────┐││ │ <Outlet /> │││ │ ← 这里渲染匹配到的子路由组件 │││ │ 如 Dashboard / UserManagement 等 │││ └────────────────────────────────────────┘│└───────────────────────────────────────────┘
布局组件示例(`src/layout/lee-basic-layout/index.tsx`):
import { Outlet } from "react-router-dom";const LeeBasicLayout = () => { return ( <Layoutstyle={{height: "100%" }}> <Header /> <Layout> <Sidebar /> <Layout> <Breadcrumb /> <Tab> <Outlet /> {/* ← 子路由在此渲染 */} </Tab> </Layout> </Layout> </Layout> );};
对于纯目录节点(如 `system-management` 只有 meta 没有 component),框架在路由渲染时自动为其注入 `<Outlet />`,使子路由能够正常渲染:
// route-utils.tsx 中的逻辑const getRouteElement = (route: RouteItem): ReactNode => { if (!route.component) { if (route.children && route.children.length > 0) { return <Outlet />; // 目录节点自动注入 Outlet } return null; } // ...懒加载处理};
4. 路由版本演进
4.1 LeeRouter V1 — 静态硬编码
最早期的版本,所有路由通过 JSX 硬编码,不依赖数据配置:
const LeeRouter = () => ( <Routes> <Routepath="/login"element={<Login />} /> <Routepath="/"element={<LayoutManager />}> <Routeindexelement={<Navigateto="/dashboard"replace />} /> <Routepath="dashboard"element={<Dashboard />} /> <Routepath="user-management"element={<UserManagement />} /> </Route> <Routepath="*"element={<Error404 />} /> </Routes>);
**适用场景:** Demo 验证、新人理解路由概念。已弃用但保留为参考。
4.2 LeeRouter V2 — 数据驱动渲染
将路由配置抽取为数据结构,通过 `renderRoutesWithNotFound()` 递归渲染为 `<Route>` 树:
const LeeRouterV2 = ({ routes }: { routes: RouteItem[] }) => { return ( <Routes>{renderRoutesWithNotFound(routes)}</Routes> );};
渲染流程:
1. 遍历 `RouteItem[]` 数组
2. 纯重定向 → `<Route element={<Navigate />} />`
3. 有子路由 → 嵌套 `<Route>`,递归渲染 children
4. 有 `redirect` + `children` → 添加 `<Route index>` 默认重定向
5. 叶子节点 → `React.lazy()` + `<Suspense>` 懒加载
6. 末尾追加 `<Route path="*">` 兜底 404
4.3 LeeRouter V3 — 后端接口控制(当前版本)
当前生产版本。使用 React Router 官方 `useRoutes` Hook + Zustand Store 管理路由,支持后端动态下发路由:
const LeeRouterV3 = ({ routes }: { routes: RouteItem[] }) => { const { getRoutes } = useRouterStore(); let newRoutes = getRoutes(); // 刷新恢复:从 menuStore 持久化数据 + 权限系统重新初始化 if (newRoutes.length <= 0) { const useMenu = useMenuStore.getState().routesMenu; initLeePermission({ ... }); newRoutes = useMenu.routesMenu || routes; } // 转换 + 渲染 const routeConfig = useMemo(() => { const transformed = transformToRouteObject(newRoutes); return addNotFoundRoute(transformed); }, [newRoutes]); return useRoutes(routeConfig);};
与 V2 的核心区别:
| 维度 | V2 | V3 ||------|----|----|| 渲染方式 | 递归 JSX `<Route>` | `useRoutes()` Hook || 路由来源 | 纯前端配置 | Zustand Store(支持后端下发) || 刷新恢复 | 无 | `menuStore` 持久化 + 权限重新初始化 || 权限控制 | 无 | `filterRoutesByPerm()` 过滤 |
5. 路由跳转
5.1 普通页面跳转
在 React 组件中 使用 `useNavigate` Hook:
import { useNavigate } from "react-router-dom";const MyComponent = () => { const navigate = useNavigate(); // 跳转到指定路径 const goToDashboard = () => { navigate("/dashboard"); }; // 替换当前历史记录(不可后退) const goToLogin = () => { navigate("/login", { replace: true }); }; // 返回上一页 const goBack = () => { navigate(-1); }; return <ButtononClick={goToDashboard}>去首页</Button>;};
5.2 带参页面跳转
本项目主要使用 Query 参数(Search Params) 传参:
发送参数端:
// src/pages/system-management/dict-management/index.tsxconst navigate = useNavigate();const handleToDictData = (record: DictTypeRecord) => { navigate(`/system-management/dict-data?dictId=${record.dictId}&dictType=${record.dictType}`);};
接收参数端:
// src/pages/system-management/dict-management/dict-data.tsximport { useSearchParams } from "react-router-dom";const DictData = () => { const [searchParams] = useSearchParams(); const dictType = searchParams.get("dictType") || "sys_user_sex"; const dictId = searchParams.get("dictId"); // ...};
其他传参方式对照表:
| 方式 | 写法 | 取值 | 特点 ||------|------|------|------|| Query 参数 | `navigate("/page?id=1")` | `useSearchParams()` | URL 可见,刷新保留 ✅ || State 参数 | `navigate("/page", { state: { id: 1 } })` | `useLocation().state` | URL 不可见,刷新丢失 ⚠️ || 动态路由参数 | `path: "user/:id"` → `navigate("/user/123")` | `useParams()` | 需配置路由 path |
5.3 非组件环境跳转 — NavigationService 降级方案
在 Axios 拦截器、Zustand Store、工具函数等非 React 组件环境中,`useNavigate` Hook 不可用。本项目通过 `NavigationService` 单例解决:
原理:在路由组件初始化时,将 `useNavigate()` 返回的函数注入到全局单例中,供非组件代码调用。
① 初始化(自动完成,无需手动操作):
// src/layout/router/index.tsx — LeeRouterV3 中自动初始化useEffect(() => { navigationService.setNavigate(navigate);}, [navigate]);
② NavigationService 实现:
// src/layout/utils/navigation.tsclass NavigationService { private navigate: ((path: string, options?: any) => void) | null = null; setNavigate(navigateFn: (path: string, options?: any) => void) { this.navigate = navigateFn; } push(path: string) { if (this.navigate) { this.navigate(path); } else { console.warn("Navigate function not set, falling back to window.location"); window.location.href = path; // 降级方案 } } replace(path: string) { if (this.navigate) { this.navigate(path, { replace: true }); } else { window.location.replace(path); // 降级方案 } }}export const navigationService = new NavigationService();
③ 实际使用场景:
// Axios 响应拦截器中(src/layout/utils/request.ts)import { navigationService } from "./navigation";// HTTP 403 → 跳转 403 错误页setTimeout(() => navigationService.replace("/403"), 1500);// HTTP 401 → 跳转登录页setTimeout(() => navigationService.replace("/login"), 1500);
// Zustand Store 中(src/store/user.ts)import { navigationService } from "@/layout/utils/navigation";// 登出后跳转登录页navigationService.replace("/login");
6. 懒加载 & Suspense
路由组件全部采用懒加载,框架层面处理了缓存优化:
// 全局缓存 Map,避免重复创建 lazy 组件const lazyCache = new Map<string, LazyExoticComponent<ComponentType>>();const getLazyComponent = (key: string, loader: LazyLoader) => { const cached = lazyCache.get(key); if (cached) return cached; const Comp = lazy(loader); lazyCache.set(key, Comp); // 以 route.name 或 route.path 作为 key return Comp;};
为什么需要缓存?
如果每次路由渲染都调用 `lazy()`,会生成新的组件引用,导致 React 认为是不同组件,触发不必要的卸载/重新挂载。缓存保证同一路由始终使用同一 lazy 组件实例。
7. 关键文件索引
| 文件 | 职责 ||------|------|| `src/App.tsx` | 应用入口,挂载 `<BrowserRouter>` || `src/router/index.tsx` | AppRouter,注入路由配置到 LeeRouter || `src/router/routes.ts` | **业务路由配置**(新增页面在这里操作) || `src/layout/router/index.tsx` | LeeRouter 引擎(V1/V2/V3) || `src/layout/router/router-type.ts` | RouteItem / RouteMeta / MenuItem 类型定义 || `src/layout/router/route-utils.tsx` | V2 路由渲染工具 || `src/layout/router/route-utils.ts` | V3 路由转换工具(transformToRouteObject) || `src/layout/utils/navigation.ts` | NavigationService 非组件跳转 || `src/layout/index.tsx` | 布局管理器(多布局切换) |
结语
如需了解权限系统、菜单、页签、KeepAlive 等由路由衍生的扩展功能,请查看下一篇 React Router的场景应用