第四章、路由配置
路由配置
一、安装依赖
npm install react-router-dom
二、推荐目录结构
src/
├── router/
│ └── index.tsx # 路由配置文件
├── pages/
│ ├── Home.tsx
│ ├── About/
│ │ ├── index.tsx
│ │ ├── LanguageI18n.tsx
│ │ └── ThemeSwitcher.tsx
│ ├── User/
│ │ ├── UserList.tsx
│ │ └── UserDetail.tsx
├── layout/
│ └── MainLayout.tsx # 公共布局
├── App.tsx
└── index.tsx
三、基础路由配置(src/router/index.tsx)
import React from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import MainLayout from "@/layout/MainLayout";
import Home from "@/pages/Home";
import About from "@/pages/About/About";
import Company from "@/pages/About/LanguageI18n";
import Team from "@/pages/About/ThemeSwitcher";
import UserList from "@/pages/User/UserList";
import UserDetail from "@/pages/User/UserDetail";
const router = createBrowserRouter([
{
path: "/",
element: <MainLayout />,// 公共布局
children: [
{ index: true, element: <Home /> },// 默认路由
{
path: "about",
element: <About />,
children: [
{ index: true, element: <Navigate to="languageI18n" replace /> },//设置默认子路由
{ path: "languageI18n", element: <LanguageI18n /> },
{ path: "themeSwitcher", element: <ThemeSwitcher /> },
],
},
{ path: "users", element: <UserList /> },
{ path: "users/:id", element: <UserDetail /> },// 动态参数
],
},
]);
export default function AppRouter() {
return <RouterProvider router={router} />;
}
四、在入口文件中加载路由
src/index.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import "./styles/globals.css";
import "./index.css";
import { ThemeProvider } from "./context/ThemeContext";
import "./i18n";
import AppRouter from "./router";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider>
<AppRouter />
</ThemeProvider>
</React.StrictMode>
);
五、导航与跳转示例
src/pages/Home.tsx
import React from "react";
import { Link, useNavigate } from "react-router-dom";
const Home: React.FC = () => {
const navigate = useNavigate();
const goToUser = () => {
navigate("/users/42");
};
return (
<div>
<h1>🏠 Home</h1>
<p>欢迎来到首页</p>
<Link to="/about">去关于页</Link>
<br />
<button onClick={goToUser}>跳转到用户详情</button>
</div>
);
};
export default Home;
六、动态路由参数获取
src/pages/User/UserDetail.tsx
import React from "react";
import { useParams } from "react-router-dom";
const UserDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
return (
<div>
<h2>用户详情页</h2>
<p>当前用户ID: {id}</p>
</div>
);
};
export default UserDetail;
七、嵌套路由(src/pages/About/index.tsx)
index.tsx 作为父级页面,需要提供导航入口,并包含一个 <Outlet /> 来渲染子页面内容。
import React from "react";
import { Link, Outlet } from "react-router-dom";
const About: React.FC = () => {
return (
<div>
<h1>关于</h1>
<nav style={{ marginBottom: "1rem" }}>
<Link to="languageI18n">国际化切换</Link> | <Link to="themeSwitcher">主题切换</Link>
</nav>
{/* 子路由出口 */}
<Outlet />
</div>
);
};
export default About;
九、子页面
src/pages/About/LanguageI18n.tsx
import LanguageSwitcher from "@/components/LanguageSwitcher";
import React from "react";
import { useTranslation } from "react-i18next";
const LanguageI18n: React.FC = () => {
const { t } = useTranslation();
return (
<div className="p-8">
<h1>{t("welcome")}</h1>
<p>
{t("language")}: {t("change_language")}
</p>
<LanguageSwitcher />
</div>
);
};
export default LanguageI18n;
src/pages/About/ThemeSwitcher.tsx
import { useTheme } from "@/context/ThemeContext";
import React from "react";
const ThemeSwitcher: React.FC = () => {
const { mode, theme, setMode, toggleTheme } = useTheme();
return (
<div style={{ padding: "2rem" }}>
<h1>🌗 React 三种主题模式</h1>
<p>当前模式:{mode}</p>
<p>当前实际主题:{theme}</p>
<div style={{ display: "flex", gap: "10px", marginBottom: "20px" }}>
<button onClick={() => setMode("light")}>亮色模式</button>
<button onClick={() => setMode("dark")}>暗色模式</button>
<button onClick={() => setMode("system")}>跟随系统</button>
<button onClick={toggleTheme}>手动切换主题</button>
</div>
<p>示例文字会根据主题自动变色。</p>
</div>
);
};
export default ThemeSwitcher;
十、路由懒加载(代码分割)
一、为什么要用懒加载?
推荐:React.lazy + Suspense 在开发中,React 项目文件越来越大,如果所有页面在首次加载时都被打包下载,会导致:
- 首屏加载慢
- 打包体积大
- 用户等待时间长
解决方案: 按需加载(代码分割)
只有当用户访问某个路由页面时,才异步加载该页面代码。
这就是 React.lazy + Suspense 的核心功能。
二、改造后的 src/router/index.tsx
在(src/router/index.tsx)中
import React, { Suspense, lazy } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// 使用 React.lazy 动态导入组件
const MainLayout = lazy(() => import("@/layout/MainLayout"));
const Home = lazy(() => import("@/pages/Home"));
const About = lazy(() => import("@/pages/About"));
const LanguageI18n = lazy(() => import("@/pages/About/languageI18n"));
const ThemeSwitcher = lazy(() => import("@/pages/About/ThemeSwitcher"));
const UserList = lazy(() => import("@/pages/User/UserList"));
const UserDetail = lazy(() => import("@/pages/User/UserDetail"));
const router = createBrowserRouter([
{
path: "/",
element: <MainLayout />,// 公共布局
children: [
{ index: true, element: <Home /> },// 默认路由
{
path: "about",
element: <About />,
children: [
{ index: true, element: <Navigate to="languageI18n" replace /> },//设置默认子路由
{ path: "languageI18n", element: <LanguageI18n /> },
{ path: "themeSwitcher", element: <ThemeSwitcher /> },
],
},
{ path: "users", element: <UserList /> },
{ path: "users/:id", element: <UserDetail /> },// 动态参数
],
},
]);
export default function AppRouter() {
return <RouterProvider router={router} />;
}
三、全局 Suspense + Loading 动画
(1)Suspense的作用:
<Suspense fallback={<div>加载中...</div>}>
<About />
</Suspense>
-
Suspense是一个占位组件; - 当内部的
lazy组件正在异步加载时,React 会渲染fallback(占位内容); - 加载完成后,React 会自动替换为真实组件。
- 这使得用户体验更加平滑(不会白屏)。
通俗理解:
“页面在加载时显示一段提示(比如‘加载中…’),加载完再显示真正的页面。”
(2) 全局 Suspense + 优雅 Loading 动画 的懒加载版本。
src/
├── router/
│ └── index.tsx # 路由配置
├── components/
│ └── Loading.tsx # 全局加载动画
├── layout/
│ └── MainLayout.tsx
└── index.tsx
全局加载动画组件src/components/Loading.tsx
import React from "react";
const Loading: React.FC = () => {
return (
<div
style={{
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
fontSize: "18px",
color: "#555",
}}
>
<div
style={{
width: 40,
height: 40,
border: "4px solid #ccc",
borderTopColor: "#4f46e5",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
></div>
<p style={{ marginTop: 10 }}>页面加载中...</p>
<style>
{`@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}`}
</style>
</div>
);
};
export default Loading;
src/router/index.tsx
import Loading from "@/components/Loading";
//...
// 用 Suspense 包裹整个路由系统
const AppRouter: React.FC = () => {
return (
<Suspense fallback={<Loading />}>
<RouterProvider router={router} />
</Suspense>
);
};
export default AppRouter;
全局 Suspense 的优势
✅ 只写一次 fallback,所有懒加载页面共用
✅ 不需要在每个 <Route> 包 <Suspense>
✅ 结构更清晰,可直接替换为统一的动画或骨架屏
十一、其他问题
1.别名使用
在
import MainLayout from "@/layout/MainLayout";时却报错。 这个问题通常是因为 TypeScript 编译器无法识别 Webpack 的别名设置。Webpack 在打包时会处理这些别名,但 TypeScript 在编译时也需要知道这些别名。需要同时在 Webpack 和 TypeScript 中配置别名。
修改 tsconfig.json
{
"compilerOptions": {
...
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
完善 Webpack 配置webpack.common.js
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, '../src'),
},
},
注意补充完后,重启一下
2.React Router DOM 最常用方法和组件表格
核心组件
| 组件 | 用途 | 示例 | 使用频率 |
|---|---|---|---|
| BrowserRouter | 应用根路由容器 | <BrowserRouter><App /></BrowserRouter> |
⭐⭐⭐⭐⭐ |
| Routes | 路由配置容器 | <Routes><Route ... /></Routes> |
⭐⭐⭐⭐⭐ |
| Route | 定义单个路由 | <Route path="/" element={<Home />} /> |
⭐⭐⭐⭐⭐ |
| Link | 声明式导航链接 | <Link to="/about">关于</Link> |
⭐⭐⭐⭐⭐ |
| Outlet | 嵌套路由渲染位置 | <Outlet /> |
⭐⭐⭐⭐ |
| NavLink | 带激活状态的链接 | <NavLink to="/" className={({isActive}) => ...}> |
⭐⭐⭐ |
常用 Hooks
| Hook | 用途 | 示例 | 使用频率 |
|---|---|---|---|
| useNavigate | 编程式导航 | const navigate = useNavigate(); navigate('/path') |
⭐⭐⭐⭐⭐ |
| useParams | 获取路由参数 | const { id } = useParams(); |
⭐⭐⭐⭐⭐ |
| useLocation | 获取当前位置信息 | const location = useLocation(); |
⭐⭐⭐⭐ |
| useSearchParams | 处理URL查询参数 | const [params, setParams] = useSearchParams(); |
⭐⭐⭐⭐ |
| useRoutes | 对象形式定义路由 | const routes = useRoutes([...]); |
⭐⭐⭐ |
导航组件对比
| 特性 | Link | NavLink | useNavigate |
|---|---|---|---|
| 类型 | 声明式 | 声明式 | 编程式 |
| 激活状态 | ❌ | ✅ | ❌ |
| 使用场景 | 普通链接 | 导航菜单 | 事件处理、表单提交 |
参数获取对比
| 场景 | 使用 Hook | 示例 |
|---|---|---|
| 路径参数 | useParams |
/user/:id → const {id} = useParams()
|
| 查询参数 | useSearchParams |
?page=1 → const [params] = useSearchParams()
|
| 状态传递 | useLocation |
navigate('/path', {state: {data}}) |
路由配置及菜单数据
(1)分清楚路由配置的数据和菜单导航的渲染数据
在菜单渲染数据里子菜单漏写父路径会导致404
![]()
(2)<Outlet />子路由出口
在src/pages/About/index.tsx中
![]()
![]()
在MainLayout.tsx中
首页、关于、用户等都是子路由
![]()
所以MainLayout.tsx是公共布局,主布局页面