第十一章. 导航核心:集成 React Router v7 与企业级架构
第十一章. 导航核心:集成 React Router v7 与企业级架构
Prorise第十一章. 导航核心:集成 React Router v7 与企业级架构
到目前为止,Prorise-Admin 还只是一个“单页”的 React 应用。在本章中,我们将为其安装“神经系统”——react-router-dom v7。
本章的目标不仅仅是“添加几个页面”,而是要深入理解 React Router v7 数据驱动 的设计哲学,并从一开始就搭建一个具备 高性能(路由懒加载)和 高可维护性(sections 拆分架构)的企业级路由骨架。
11.1. 奠基:从静态应用到数据路由 SPA
11.1.1. 痛点:为什么需要客户端路由?
目前,main.tsx 只是一个静态渲染 <MyApp /> 的入口。它无法区分 http://localhost:5173/ 和 http://localhost:5173/login。无论 URL 如何变化,用户看到的都是同一个页面。这不是一个“应用”,只是一个“组件”。
传统多页应用 (MPA) 的做法是:每次 URL 变化(例如点击链接),浏览器都会向服务器发起一个 全新的页面请求,服务器返回一个全新的 HTML 文档。这会导致页面“白屏”、状态丢失、体验割裂。
单页应用 (SPA) 的解决方案是:
- 只在首次访问时加载一个 HTML“壳”。
- 此后,所有 URL 变化都由 客户端 JavaScript(即 React Router)接管。
- React Router 会 拦截 导航事件,阻止 浏览器发起新请求。
- 它在 客户端 匹配 URL,动态地卸载旧组件、挂载新组件。
这带来了流畅的、无刷新的“应用级”体验。
11.1.2. 为什么选择 createBrowserRouter?
在 React Router v5 及更早版本中,路由是通过 组件 来定义的,例如 <BrowserRouter><Route path="/" component={Home} /></BrowserRouter>。
React Router v6/v7 带来了 核心变革:从“组件即路由”转向“配置即路由”。
我们选择 v7 的核心 API createBrowserRouter,因为它是 2025 年的最佳实践。它创建的不是一个组件,而是一个 数据路由引擎。一个路由对象 ({ path: ... }) 现在可以集中定义:
Component:渲染什么组件。loader:(数据驱动核心) 在渲染前 加载什么数据。action:(数据驱动核心) 如何 处理此路由上的表单提交(我们将在第 14 章的登录表单中深入使用)。errorElement:组件、loader或action在执行出错时,显示什么降级 UI。
这种“配置即路由”的模式,使路由配置更集中、更声明式,并且从一开始就具备了处理数据和错误的能力。
11.1.3. (编码) 安装依赖
我们开始本章的第一个编码任务:安装 react-router-dom。
1 | pnpm add react-router-dom |
11.1.4. (编码) 创建根路由 index.tsx
我们的所有路由配置都将存放在 src/routes/ 目录下。创建 index.tsx 文件,这是路由系统的 入口。
文件路径: src/routes/index.tsx
1. 导入核心 API:
1 | import { createBrowserRouter } from 'react-router-dom'; |
2. 创建路由配置数组:
1 | // 这是一个临时的测试页面,稍后会删除 |
思考:
Component: MyApp:我们将<MyApp />(第六章ThemeProvider的封装)作为 根布局组件。children: 这是嵌套路由的核心。react-router-domv7 会将children中的element(如<WelcomePage />)渲染到父组件 (MyApp) 内部的一个<Outlet />组件中。index: true:这指定了当 URL 匹配到父路径 (/) 时,默认应该渲染哪个子路由。
11.1.5. (编码) 重构 main.tsx 应用入口
现在,router 实例已经创建,它成为了应用的新“大脑”。我们必须修改 src/main.tsx,让 React 渲染这个 router,而不是直接渲染 <MyApp />。
文件路径: src/main.tsx
1 | import React from 'react'; |
架构解析:
main.tsx的职责:main.tsx现在的唯一职责就是:启动 React,并将根router实例交给<RouterProvider />。<RouterProvider />:这是react-router-domv7 的 根组件。它会接管应用的渲染,根据当前的 URL,从router配置中找到匹配的路由对象,并渲染对应的Component。
验证:保存所有文件并运行 pnpm dev。
- 访问
http://localhost:5173/。 - 你会看到
<WelcomePage />的内容(“欢迎来到 Prorise-Admin”)被渲染了。 - 打开 React DevTools,你会看到组件树是
<RouterProvider>-><MyApp>-><WelcomePage />。 - 这证明我们的路由系统工作了:
router匹配了/,渲染了根组件<MyApp />,然后又匹配了index: true,将<WelcomePage />渲染到了<MyApp />的<Outlet />(我们下一节添加)中。
任务 11.1 已完成。
我们成功地将 Prorise-Admin 从一个静态组件升级为了一个由 react-router-dom v7 驱动的、真正的 SPA 应用。
11.2. 性能优化:路由级代码分割与 Suspense 集成
在 11.1 节中,项目成功集成了 react-router-dom v7,将静态的 React 应用转变为具备基本导航能力的单页应用(SPA)。然而,当前的实现方式隐藏着一个 严重的性能隐患,必须在项目早期就予以解决,否则随着应用的增长,将导致灾难性的用户体验。
11.2.1 性能瓶颈:静态导入与初始包体积膨胀
让我们回顾 11.1 节的核心代码片段:
文件路径: src/routes/index.tsx (11.1 版本)
1 | import { createBrowserRouter } from 'react-router-dom'; |
问题在哪里?
这里的 import MyApp from '@/MyApp'; 和直接使用的 <WelcomePage /> 都属于 静态导入。对于现代 JavaScript 打包工具(如 Vite 或 Webpack)而言,静态导入意味着:“这个模块(及其所有依赖)在应用启动时就必须可用。”
因此,当 Vite 构建生产版本时,它会进行依赖分析,将 MyApp.tsx、WelcomePage.tsx 以及它们递归导入的 所有 代码——包括庞大的 UI 库(如 Ant Design)、图标库、日期处理库、图表库等等——全部打包进一个或少数几个 初始 JavaScript 文件(通常称为 main.js 或 vendor.js)。
后果是什么?
- 对于小型应用(几个页面):这通常不是问题,初始包体积可能只有几百 KB。
- 对于企业级后台(数十甚至上百个页面):这将导致初始
main.js文件 极其臃肿,体积轻易达到 数 MB 甚至十几 MB。
当用户首次访问应用时,浏览器必须 完整下载、解析并执行 这个巨大的 main.js 文件,然后才能渲染出第一个有意义的界面(First Meaningful Paint, FMP)并响应用户交互(Time to Interactive, TTI)。
用户的直观感受就是:点击链接后长时间的 白屏,或者页面出来了但按钮点击没反应。这对于需要高效率操作的企业级后台应用来说,是 完全不可接受的。随着应用功能的迭代,这个问题只会越来越严重,最终成为项目性能的主要瓶颈。
11.2.2 核心解决方案:路由级代码分割
要解决初始包体积膨胀的问题,必须实施 代码分割。代码分割的核心思想是:“不要一次性加载所有代码,只在需要时加载对应的代码。”
对于 SPA 应用,最有效、最常用的代码分割策略就是 路由级代码分割。这意味着:
- 只有当用户访问
/dashboard路由时,才去下载DashboardPage.js及其相关依赖的代码块 (chunk)。 - 只有当用户访问
/system/users路由时,才去下载UserManagementPage.js及其相关依赖的代码块。
这样,应用的初始加载体积可以被 显著减小,只包含核心框架、根布局和当前访问路由所需的最小代码集。其他页面的代码将在用户导航到对应路由时 按需、并行 地加载。
11.2.3 实现机制:React.lazy 与动态 import()
React 官方提供了 React.lazy() 函数,专门用于实现组件级别的代码分割。它与 JavaScript 的 动态 import() 语法(注意是函数调用 import() 而不是顶部的 import ... from ... 语句)协同工作。
转换过程:
静态导入 (打包进主文件):
1
import WelcomePage from '@/pages/Welcome';
动态导入 (触发代码分割):
1
2
3
4
5
6
7
8
9
10import { lazy } from 'react';
// 1. 调用 lazy() 函数
const LazyWelcomePage = lazy(
// 2. 传入一个函数,该函数调用动态 import()
() => import('@/pages/Welcome')
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 这个 import() 返回一个 Promise,
// 该 Promise 会在浏览器成功下载并解析 '@ /pages/Welcome' 模块后 resolve
);
LazyWelcomePage 现在是一个特殊的 “可挂起 (Suspendable)” 组件。当 React Router 尝试渲染它时:
- React 检测到这是一个
lazy组件。 - 它 暂停 (Suspend) 当前的渲染流程。
- 它 触发 内部的
import('@/pages/Welcome')调用。 - 浏览器 开始异步下载 由 Vite 自动分割出来的
Welcome.js代码块 (chunk)。 - 下载并解析完成后,
import()返回的 Promise 被 resolve。 - React 恢复 渲染,将真正的
<WelcomePage />组件渲染到 DOM 中。
11.2.4 处理加载状态:React.Suspense 与占位符 UI
在 React.lazy 触发的异步加载期间(“暂停”状态),用户界面不能显示为空白或卡顿。必须提供一个 加载指示器 (Loading Indicator) 或 占位符 UI (Placeholder UI)。
React 提供了 <Suspense> 组件来解决这个问题。Suspense 是一个 边界 (Boundary) 组件,它可以捕获其子树中任何组件(包括 lazy 组件)抛出的“暂停”信号。
工作流程:
- 使用
<Suspense>组件包裹可能触发暂停的组件。 - 为
<Suspense>提供一个fallbackprop,其值是一个 React 元素(例如,一个加载动画)。
1 | import React, { Suspense, lazy } from 'react'; |
当 LazyWelcomePage 加载完成并恢复渲染时,Suspense 会自动用 <WelcomePage /> 替换掉 fallback UI。
架构策略:对于路由懒加载,最佳实践是将 <Suspense> 放置在路由树中尽可能高的位置,通常是在渲染子路由的 <Outlet /> 组件周围。这样,一个 <Suspense> 就可以处理 所有 子路由的加载状态。
11.2.5 (编码) 创建路由加载占位符 RouteLoading.tsx
现在,开始实现 Suspense 需要的 fallback UI。我们将实现一个 简洁、职责单一 的加载组件。它不关心加载进度或 URL 变化,只负责“显示加载状态”,状态管理完全交给 React Suspense。
1. 创建文件:
在 src/components/loading/ 目录下创建 route-loading.tsx 文件。
1 | touch src/components/loading/route-loading.tsx |
2. 导入依赖:
为了提供清晰的视觉反馈,可以使用项目已集成的 Ant Design UI 库中的 <Spin /> 组件。
1 | // src/components/loading/route-loading.tsx |
3. 实现组件:
目标是在屏幕中央显示一个旋转的加载动画。
1 | // src/components/loading/route-loading.tsx |
设计哲学剖析:
- 简洁性与职责单一:这个
RouteLoading组件极其简单。它 不 包含useState,useEffect,MutationObserver,setInterval或任何与路由状态相关的逻辑。它仅仅是一个纯粹的 UI 展示组件。 - 关注点分离 (SoC):它将“何时显示/隐藏加载状态”的复杂逻辑完全委托给了 React 的
Suspense机制。Suspense负责感知lazy组件的加载状态,RouteLoading只负责在被要求渲染时,画出那个加载动画。 - 可维护性:这种解耦使得
RouteLoading非常容易维护和替换。如果将来需要更换加载动画样式,只需修改这一个文件,而无需触及任何路由或状态管理逻辑。 - 性能:由于其简单性,它自身的性能开销极低。
11.2.6 (编码) 重构根组件 MyApp.tsx 以集成 Suspense 和 <Outlet>
RouteLoading 组件已准备就绪。下一步是修改根路由组件 MyApp.tsx,将其集成进去。MyApp.tsx 需要完成两件事:
- 引入
React.Suspense并使用RouteLoading作为fallback。 - 引入
react-router-dom的<Outlet />组件,指定子路由应该渲染的位置。
文件路径: src/MyApp.tsx
1 | // 1. 导入 Suspense (来自 React) 和 Outlet (来自 React Router) |
架构职责明确:
MyApp.tsx现在清晰地承担了 根布局 和 全局上下文提供者 的角色。- 它通过
<Outlet />定义了子路由的“坑位”。 - 它通过
<Suspense>定义了所有子路由加载时的 统一视觉反馈。
11.2.7 重构路由配置 index.tsx 以应用 React.lazy
万事俱备,只欠东风。最后一步是将 src/routes/index.tsx 中的路由组件改为使用 React.lazy 进行动态导入。
1. 创建占位页面组件:
为了让 lazy 函数能工作,需要先创建它要加载的文件。
1 | mkdir -p src/pages |
文件路径: src/pages/Welcome.tsx (临时内容)
1 | export default function WelcomePage() { |
2. 修改路由配置文件:
文件路径: src/routes/index.tsx
1 | import { lazy } from "react"; |
- 加载流程:
- 用户访问
/。 RouterProvider匹配到根路由,需要渲染LazyMyApp。- React 暂停,触发
import('@/MyApp'),下载MyApp.chunk.js。- (此时,由于
RouterProvider外部没有Suspense,用户可能会看到短暂白屏或浏览器默认加载状态,这是正常的,根组件加载无法避免)
- (此时,由于
MyApp加载完成,开始渲染。MyApp渲染到<Suspense fallback={<RouteLoading />}> <Outlet /> </Suspense>。<Outlet />需要渲染LazyWelcomePage(因为index: true)。- React 再次暂停,触发
import('@/pages/Welcome'),下载Welcome.chunk.js。 - 在下载
Welcome.chunk.js期间,MyApp内部的Suspense捕获 到暂停,渲染<RouteLoading />。 WelcomePage加载完成,Suspense自动切换,渲染<WelcomePage />。
- 用户访问
11.2.8 验证实现效果
- 确保
src/pages/Welcome.tsx已创建。 - 运行
pnpm dev。 - 打开浏览器开发者工具 (F12),切换到“网络 (Network)”选项卡,勾选“禁用缓存 (Disable cache)”。
- 访问
http://localhost:5173/。 - 观察网络请求:
- 除了
main.tsx,@vite/client等核心文件。 - 会看到浏览器 异步请求 了类似
_MyApp.js和_Welcome.js(文件名可能带有 hash) 的两个 JavaScript 文件。这证明代码分割成功!
- 除了
- 观察界面:
- 在页面内容(“欢迎…”)出现之前,会 短暂地看到
RouteLoading组件(Antd Spin)在屏幕中央旋转。这证明Suspense和fallback工作正常!
- 在页面内容(“欢迎…”)出现之前,会 短暂地看到
- 再次刷新 (保持禁用缓存):会重复看到加载指示器和网络请求,因为每次都强制重新下载。
- 取消勾选“禁用缓存”后刷新:加载指示器几乎不可见,页面瞬时出现。网络请求状态码变为 304 (Not Modified) 或 200 (from disk cache/memory cache),证明浏览器缓存生效。
任务 11.2 已完成!
项目现在具备了企业级的路由懒加载能力。所有通过 React.lazy 加载的页面组件都会自动进行代码分割,并通过根布局中的 Suspense 和 RouteLoading 提供统一、流畅的加载过渡体验。这为后续添加更多页面奠定了坚实的性能基础。
11.3. 错误处理:基础 ErrorBoundary 实现
在 11.1 和 11.2 节中,项目集成了 createBrowserRouter 并实现了路由懒加载。但目前的实现还缺少一个关键环节:错误处理。
11.3.1. 分析:路由错误的潜在风险
当前的路由系统存在风险:
- 懒加载失败:如果
React.lazy在import('@/MyApp')或import('@/pages/Welcome')时遇到网络错误(例如,对应的 chunk 文件加载失败),应用可能会崩溃或显示一个不友好的原生错误。 - 渲染错误:如果
MyApp或WelcomePage组件在渲染过程中抛出 JavaScript 错误,同样可能导致应用白屏。 - Loader/Action 错误 (未来):当后续章节为路由添加
loader(数据加载) 或action(表单处理) 函数后,这些函数执行出错时,也需要有机制来捕获并展示错误信息。
react-router-dom v7 的 createBrowserRouter 通过 errorElement 配置项,提供了一个内置的、声明式的解决方案来处理这些路由相关的错误。
errorElement 的作用是:当路由本身或其子路由在 渲染、加载数据 (loader) 或 执行操作 (action) 时发生错误,React Router 会 捕获 这个错误,并 渲染 指定的 errorElement 组件,而不是让整个应用崩溃。
因此,必须在路由配置中(尤其是在根路由层级)提供一个 errorElement。
11.3.2. (编码) 创建基础错误边界组件目录与文件
将把路由相关的辅助组件(如错误边界、认证守卫等)统一放在 src/routes/components/ 目录下。
1 | mkdir -p src/routes/components |
11.3.3. (编码与精讲) 实现基础版 ErrorBoundary
现在,将实现 error-boundary.tsx。这个 基础版本 的目标是:
- 使用 React Router 提供的钩子获取错误信息。
- 显示最基本的错误提示(状态码、消息),不 依赖任何复杂的 UI 组件或主题变量。
1. 导入依赖:
需要从 react-router-dom 导入 useRouteError(用于获取错误对象)和 isRouteErrorResponse(用于判断错误类型)。
1 | // src/routes/components/error-boundary.tsx |
2. 实现组件逻辑:
组件的核心是调用 useRouteError,然后根据错误类型渲染不同的信息。
1 | import { Button, Result } from "antd"; |
思考过程与设计决策:
useRouteError: 这是获取路由错误的 唯一 途径。它只能在errorElement指定的组件中使用。- 错误类型判断: 区分
isRouteErrorResponse和instanceof Error很重要,因为它们携带错误信息的方式不同 (error.datavserror.message)。 - 返回首页链接: 提供一个基本的导航出口。
11.3.4. (编码) 将 ErrorBoundary 配置到根路由
现在,将这个基础版的 ErrorBoundary 应用到 src/routes/index.tsx 的根路由配置中。
文件路径: src/routes/index.tsx (更新)
1 | import { createBrowserRouter, type RouteObject } from 'react-router-dom'; |
集成完成:现在,如果在加载 LazyMyApp 或其任何子路由(如 LazyWelcomePage)时发生错误,或者这些组件在渲染时抛出错误,React Router 会捕获它,并渲染我们刚刚创建的基础 ErrorBoundary 组件,而不是白屏。
任务 11.3 已完成!
项目现在具备了 基础的路由级错误处理能力。虽然 UI 比较简陋,但核心机制已经建立。我们将在后续章节(预计第十二章或之后,待 UI 组件和主题完善)回过头来增强 这个 ErrorBoundary,使其达到企业级框架的视觉和功能水平。
11.4. 可维护性:sections 路由架构
在 11.2 和 11.3 节中,项目成功实现了路由懒加载和基础错误处理,解决了 性能 和 健壮性 的初步问题。然而,当前的路由配置文件 (src/routes/index.tsx) 存在一个巨大的 可维护性 隐患。
11.4.1. 识别可维护性瓶颈:单一配置文件的“上帝模式”
回顾 11.3 节结束时的 src/routes/index.tsx 结构:
1 | // src/routes/index.tsx (11.3 版本) |
可以预见,随着应用功能的增加,这个 routes 数组将 线性增长。一个真实的企业级后台可能包含几十甚至上百个路由配置,涉及不同的功能模块(认证、仪表盘、系统管理、用户中心、错误处理…)和复杂的嵌套关系。
将所有路由配置集中在 index.tsx 这 一个文件 中,会带来严重的问题:
- 文件冗长,难以导航:当文件达到数百甚至上千行时,查找和修改特定路由配置会变得非常耗时且容易出错。
- 可读性差:深层嵌套的路由对象会使代码难以理解和推理。
- 团队协作冲突:不同功能团队(例如,“认证”团队和“系统管理”团队)需要频繁修改同一个文件,这将导致大量的 Git 合并冲突,降低开发效率。
- 违反单一职责原则 (SRP):
index.tsx文件承担了过多的职责,成为了一个无所不包的“上帝文件 (God File)”,难以维护和测试。
11.4.2. 引入架构解决方案:sections 路由拆分
为了解决单一配置文件的可维护性瓶颈,必须引入“关注点分离 (Separation of Concerns)”的设计原则。
大多数项目采用的 src/routes/sections 模式是实践这一原则的优秀架构。
核心思想:
- 根路由文件 (
index.tsx) 不再负责 定义具体的页面路由。 index.tsx的职责转变为:- 定义应用的 根布局(
LazyMyApp)和 根错误边界(ErrorBoundary)。 - 组装 (Assemble) 来自不同功能模块(称为 “Sections”)的、独立的路由配置数组。
- 定义应用的 根布局(
- 每一个独立的功能域(例如,认证、仪表盘、公共页面)都在
src/routes/sections/目录下拥有 自己的、独立的 路由配置文件,每个文件导出一个RouteObject[]数组。
这种架构就像一个模块化的系统,index.tsx 是主板,而各个 sections/*.tsx 文件是可插拔的功能卡。
11.4.3. (编码) 创建 sections 目录结构
首先,在 src/routes/ 目录下创建 sections/ 子目录。
1 | mkdir -p src/routes/sections |
接下来,根据企业级后台的常见功能域,创建对应的路由配置文件。初期,文件可以只包含骨架:
main.tsx: 用于定义 非功能性、公共 的路由,如 404 页面、500 错误页面,或通用入口/占位页面(如WelcomePage)。auth.tsx: 专门负责 认证 相关的路由,如/login,/register,/forgot-password等。dashboard.tsx: 负责 核心后台功能 的路由,通常包含仪表盘布局和所有受保护的功能页面。
1 | touch src/routes/sections/main.tsx |
11.4.4. (编码) 实现 main.tsx (公共路由)
将之前放在 index.tsx 中的 WelcomePage 路由配置,以及新增一个 404 页面路由,迁移 到 main.tsx 中。
1. 创建 404 页面组件:
1 | mkdir -p src/pages/sys/error |
文件路径: src/pages/sys/error/Page404.tsx (基础实现)
1 | import { Button, Result } from "antd"; |
2. 实现 main.tsx 路由配置:
文件路径: src/routes/sections/main.tsx
1 | import { lazy } from 'react'; |
思考过程:main.tsx 现在清晰地承担了定义“公共路由”的职责。WelcomePage (首页占位) 和 NotFoundPage (404) 都属于公共范畴。
11.4.5. (编码) 实现 auth.tsx 和 dashboard.tsx (占位符)
这两个文件目前还没有实际的页面可以配置,因此暂时导出空数组,但文件的存在本身就定义了清晰的架构扩展点。
文件路径: src/routes/sections/auth.tsx
1 | import { type RouteObject } from 'react-router-dom'; |
文件路径: src/routes/sections/dashboard.tsx
1 | import { type RouteObject } from 'react-router-dom'; |
11.4.6. (编码) 重构 index.tsx 为“路由组装器”
最关键的一步:重构 src/routes/index.tsx,移除具体的页面路由定义,转变为只负责 导入 和 组装 各个 sections 的路由数组。
文件路径: src/routes/index.tsx (重构后)
1 | import { createBrowserRouter, type RouteObject } from 'react-router-dom'; |
架构优势总结:
- 高度内聚:与“认证”相关的所有路由配置都内聚在
auth.tsx中。 - 低耦合:修改
auth.tsx完全不影响dashboard.tsx或index.tsx(除非修改导出的变量名)。 - 可读性强:
index.tsx现在非常简洁,清晰地展示了应用的顶层结构和包含哪些功能模块。 - 易于扩展:添加新功能模块(如“设置中心”)只需:
- 创建
sections/settings.tsx并导出settingsRoutes。 - 在
index.tsx中import并在children中展开...settingsRoutes。
- 创建
- 团队协作友好:不同团队可以安全地并行开发各自负责的
section文件,合并冲突的风险降至最低。
11.4.7 验证 sections 架构
- 确保已创建
src/pages/sys/error/Page404.tsx文件。 - 运行
pnpm dev。 - 访问
http://localhost:5173/:应该看到WelcomePage(来自mainRoutes的index: true)。 - 访问
http://localhost:5173/some-random-path:应该看到NotFoundPage(来自mainRoutes的path: '*')。 - 检查网络请求:懒加载依然有效,访问
/时会加载Welcome.chunk.js,访问/random时会加载Page404.chunk.js。
任务 11.4 已完成!
项目现在拥有了一个 高度可维护、可扩展的企业级路由架构。sections 模式为后续的功能迭代和团队协作奠定了坚实的基础。
11.5. 开发者体验:封装路由钩子与 Logo 修正
在 11.4 节中,项目成功建立了 sections 路由架构,解决了可维护性的核心问题。本节将聚焦于提升开发者体验 (DX),通过封装自定义路由钩子,提供更简洁、更符合人体工程学的 API,并修正之前章节遗留的一个小问题。
11.5.1. 修正 Logo 组件:添加导航功能
目前 src/components/logo/Logo.tsx 还是一个纯展示组件(根元素是 <span>),无法点击。现在路由系统已就绪,需要为其添加返回首页的导航功能。
1. (编码) 导入 Link 组件:
打开 src/components/logo/Logo.tsx,导入 react-router-dom 的 <Link> 组件。
1 | // src/components/logo/Logo.tsx |
说明:选用 <Link> 而非 <NavLink>,因为 Logo 通常不需要激活状态样式。
2. (编码) 替换根元素:
将返回的根元素从 <span> 修改为 <Link to="/">。
1 | // src/components/logo/Logo.tsx |
结果:Logo 组件现在具备了完整的导航功能。
11.5.2. 识别 DX 痛点:分散的原生路由钩子
react-router-dom v7 提供了强大的钩子 (Hooks) 来访问路由状态和执行导航操作:
useNavigate(): 用于编程式导航 (Maps('/path'),Maps(-1)).useLocation(): 用于获取当前 URL 信息 (location.pathname,location.search).useParams(): 用于获取动态路由参数 (/users/:userId).useSearchParams(): 用于读取和修改 URL 查询参数 (?q=keyword).
react-router-dom v7 提供了 useNavigate, useLocation, useParams, useSearchParams 等钩子,功能强大但 API 分散。在组件中,可能需要同时导入并调用多个,显得较为繁琐。
1 | // 示例:组件内可能需要多个导入 |
11.5.3. 解决方案:封装聚合的自定义路由钩子
为了提升开发效率和代码一致性,将创建一系列自定义钩子,封装原生钩子,提供更简洁、聚合的 API。这些自定义钩子将统一放在 src/routes/hooks/ 目录下。
同时,在封装过程中,将应用 useMemo 最佳实践,确保钩子返回值的引用稳定性,这对于避免不必要的重渲染和适配未来 React 的编译优化非常重要。
11.5.4. (编码) 创建 hooks 目录与 useRouter 钩子
1. 创建目录:
1 | mkdir -p src/routes/hooks |
2. 创建 use-router.ts 文件并实现:
封装 useNavigate,提供 push, replace, back 方法。
文件路径: src/routes/hooks/use-router.ts
1 | import { useMemo } from "react"; |
说明:useMemo 能确保当组件重渲染时,从 useRouter 获取的 router 对象不会跟着重新生成,始终是同一个对象。
这对那些依赖 router 对象的 React.memo 子组件或 useEffect 来说至关重要,可以避免它们因对象引用改变而触发不必要的重新渲染或重复执行。
11.5.5. (编码) 创建 usePathname, useParams, useSearchParams 钩子 (Memoized)
继续创建其他钩子,并确保应用 useMemo。
1. 创建 use-pathname.ts:
文件路径: src/routes/hooks/use-pathname.ts
1 | import { useMemo } from 'react'; |
2. 创建 use-params.ts:
文件路径: src/routes/hooks/use-params.ts
1 | import { useMemo } from 'react'; |
3. 创建 use-search-params.ts (返回 Memoized 元组):
文件路径: src/routes/hooks/use-search-params.ts
1 | import { useMemo } from "react"; |
说明:对于 useSearchParams,选择返回与原生钩子一致的 [URLSearchParams, Function] 元组,并通过 useMemo 保证 URLSearchParams 实例和元组本身的引用稳定性。
11.5.6. (编码) 创建 hooks 的入口文件 index.ts
创建一个 “barrel” 文件 (index.ts),统一导出所有自定义钩子,方便开发者导入。
1 | touch src/routes/hooks/index.ts |
文件路径: src/routes/hooks/index.ts
1 | export * from './use-router'; |
- 这个文件是一个“打包出口”,它将所有分散在不同文件中的自定义钩子(
use-router,use-params等)集中到了一起。 - 这样做可以让其他开发者在导入时,只需要从
./hooks这一个路径导入所有需要的钩子,而不用去关心每个钩子的具体文件名。 - 最终,这让导入语句更简洁,也让项目的代码结构更清晰、更易于维护。
11.5.7. 提升开发者体验
现在,开发者可以更方便地访问路由功能:
1 | // 只需从一处导入所有需要的钩子 |
通过封装,代码变得更简洁,导入来源更统一,并且通过 useMemo 的应用,为性能和未来优化奠定了基础。
11.6. 本章小结与代码入库
在本章中,项目完成了从一个静态 React 组件到功能完备的单页应用 (SPA) 导航核心的奠基。通过集成 react-router-dom v7 并实施企业级的架构模式,为后续的功能开发铺平了道路。
核心进展回顾:
- 路由引擎集成 (11.1):引入了
react-router-domv7,使用createBrowserRouter定义了第一个数据路由,并通过<RouterProvider>替换了main.tsx的渲染入口,确立了路由驱动的应用架构。 - 性能优化 (11.2):识别了静态导入的性能瓶颈,通过
React.lazy实现了路由级的代码分割。创建了RouteLoading组件,并结合<Suspense>与<Outlet />在根布局 (MyApp.tsx) 中提供了全局、统一的页面加载过渡体验。 - 基础错误处理 (11.3):分析了路由错误的潜在风险,实现了基础版的
ErrorBoundary组件,使用useRouteError处理错误,并将其配置到根路由的errorElement,提升了应用的健壮性。 - 可维护性架构 (11.4):为了解决单一配置文件带来的维护性问题,成功实施了
sections路由拆分架构。创建了src/routes/sections/目录及main.tsx,auth.tsx,dashboard.tsx配置文件,并将根路由index.tsx重构为职责清晰的“路由组装器”。 - 开发者体验 (11.5):修正了
Logo组件,为其添加了<Link>导航功能。封装了一系列自定义路由钩子 (useRouter,usePathname,useParams,useSearchParams),应用了useMemo最佳实践,提供了更简洁、引用稳定的 API。
通过本章的工作,Prorise-Admin 现在拥有了一个现代化的、高性能且高度可维护的路由系统基础。
代码入库
将本章的所有成果提交到版本控制系统。
1. 检查代码状态:
1 | git status |
(应包含 package.json, pnpm-lock.yaml, src/main.tsx, src/MyApp.tsx, src/routes/index.tsx, src/routes/components/error-boundary.tsx, src/routes/sections/*, src/routes/hooks/*, src/components/loading/route-loading.tsx, src/components/logo/Logo.tsx, src/pages/Welcome.tsx, src/pages/sys/error/Page404.tsx 等文件的变更。)
2. 暂存所有变更:
1 | git add . |
3. 执行提交:
1 | git commit -m "feat(routes): integrate react-router v7 with sections architecture" |
第十一章已圆满完成! 项目的导航核心已经建立。













