193 lines
5.0 KiB
Markdown
193 lines
5.0 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, Router, Navigation, Frontend]
|
|||
|
|
create time: 2026-04-29 22:07
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 路由管理
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
前端路由是单页应用(SPA)的核心基础设施。本文档以 React Router v7 为主介绍路由配置、嵌套路由、懒加载、动态路由和守卫拦截等实战模式。
|
|||
|
|
|
|||
|
|
## React Router v7 核心概念
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TB
|
|||
|
|
A[BrowserRouter] --> B["哈希历史 vs HTML5 历史"]
|
|||
|
|
B --> C[RouterProvider]
|
|||
|
|
C --> D[Route 树]
|
|||
|
|
D --> E["Element / Component / ComponentFunction"]
|
|||
|
|
D --> F["Layout Route"]
|
|||
|
|
F --> G["Child Routes"]
|
|||
|
|
|
|||
|
|
style A fill:#F4DBD6,color:#000
|
|||
|
|
style E fill:#61DAFB,color:#000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 基本结构
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// app.tsx(v7 推荐入口)
|
|||
|
|
import { createRootRouteWithContext, createRouter, RouterProvider } from "@tanstack/react-router";
|
|||
|
|
// 或传统 react-router-dom v6/v7
|
|||
|
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|||
|
|
|
|||
|
|
const router = createBrowserRouter([
|
|||
|
|
{
|
|||
|
|
path: "/",
|
|||
|
|
element: <RootLayout />,
|
|||
|
|
children: [
|
|||
|
|
{ index: true, element: <HomePage /> },
|
|||
|
|
{ path: "about", element: <AboutPage /> },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
function App() {
|
|||
|
|
return <RouterProvider router={router} />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Layout Route 与嵌套路由
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
{
|
|||
|
|
// /dashboard 及其子路径共用此布局
|
|||
|
|
path: "dashboard",
|
|||
|
|
element: <DashboardLayout />, // 侧边栏 + Header 常驻
|
|||
|
|
children: [
|
|||
|
|
{ index: true, element: <DashboardHome /> }, // /dashboard → DashboardHome
|
|||
|
|
{ path: "stats", element: <StatsPage /> }, // /dashboard/stats
|
|||
|
|
{ path: "settings", element: <SettingsPage /> }, // /dashboard/settings
|
|||
|
|
{ path: "settings/:tab", element: <SettingsTab /> }, // /dashboard/settings/profile
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// DashboardLayout.tsx
|
|||
|
|
function DashboardLayout() {
|
|||
|
|
return (
|
|||
|
|
<div className="dashboard-layout">
|
|||
|
|
<Sidebar />
|
|||
|
|
<header><Navigation /></header>
|
|||
|
|
<main>
|
|||
|
|
<Outlet /> {/* v7 Outlet 替代了 v6的children渲染 */}
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] Outlet vs Children
|
|||
|
|
> - v6 用 `<Routes>` 内部定义子路由
|
|||
|
|
> - v7 用 `<Outlet />` 显式标注嵌套出口,更清晰
|
|||
|
|
|
|||
|
|
## 路由参数获取
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useSearchParams, useParams, useNavigate, useLocation } from "react-router-dom";
|
|||
|
|
|
|||
|
|
function UserPage() {
|
|||
|
|
const { id } = useParams(); // /user/:id → { id: "123" }
|
|||
|
|
const [searchParams] = useSearchParams(); // ?q=hello&page=1
|
|||
|
|
const location = useLocation(); // { pathname: "/user/123", search: "?q=hello" }
|
|||
|
|
const navigate = useNavigate(); // navigate("/home") / navigate(-1)
|
|||
|
|
|
|||
|
|
const query = searchParams.get("q");
|
|||
|
|
|
|||
|
|
return <h1>User #{id}, searching for "{query}"</h1>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### v7 新增:useRouteContext + Data APIs
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// v7 data routers 支持 loader/action
|
|||
|
|
{
|
|||
|
|
path: "posts/:postId",
|
|||
|
|
loader: async ({ params }) => {
|
|||
|
|
const res = await fetch(`/api/posts/${params.postId}`);
|
|||
|
|
return res.json();
|
|||
|
|
},
|
|||
|
|
// loader 数据自动注入 component props
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 懒加载 Code Splitting
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { lazy, Suspense } from "react";
|
|||
|
|
|
|||
|
|
// 路由级代码分割
|
|||
|
|
const AdminPage = lazy(() => import("./pages/AdminPage"));
|
|||
|
|
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
path: "admin",
|
|||
|
|
element: (
|
|||
|
|
<Suspense fallback={<PageSpinner />}>
|
|||
|
|
<AdminPage />
|
|||
|
|
</Suspense>
|
|||
|
|
),
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 懒加载时机判断
|
|||
|
|
> - 首屏路由(首页、登录页)**不要**懒加载
|
|||
|
|
> - 低频访问页面(设置、管理员面板)适合懒加载
|
|||
|
|
> - 每个 chunk 建议不超过 100KB gzipped
|
|||
|
|
|
|||
|
|
## 动态路由与 Splat 路由
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// :param —— 单个段匹配
|
|||
|
|
{ path: "users/:userId", element: <UserProfile /> } // /users/42
|
|||
|
|
|
|||
|
|
// * splat —— 贪婪匹配剩余所有
|
|||
|
|
{ path: "docs/*", element: <DocViewer /> } // /docs/a/b/c
|
|||
|
|
{ path: "*", element: <NotFoundPage /> } // 404 fallback
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 路由守卫与权限控制
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 方案1:条件渲染(简单场景)
|
|||
|
|
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|||
|
|
const { user } = useAuth();
|
|||
|
|
if (!user) return <Navigate to="/login" replace />;
|
|||
|
|
return <>{children}</>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 方案2:高阶包装
|
|||
|
|
const withAuth = (Component: React.FC) => {
|
|||
|
|
return (props: any) => {
|
|||
|
|
const { loading, authenticated } = useAuth();
|
|||
|
|
if (loading) return <Spinner />;
|
|||
|
|
if (!authenticated) return <Navigate to="/login" replace />;
|
|||
|
|
return <Component {...props} />;
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 方案3:v7 Loader 守卫(服务端前置检查)
|
|||
|
|
{
|
|||
|
|
path: "admin",
|
|||
|
|
loader: () => {
|
|||
|
|
if (!isAuthenticated()) throw new Response("", { status: 401 });
|
|||
|
|
if (!isAdmin()) throw new Response("", { status: 403 });
|
|||
|
|
},
|
|||
|
|
element: <AdminPanel />,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 导航 API 对比
|
|||
|
|
|
|||
|
|
| 方法 | 适用场景 | 是否保留历史记录 |
|
|||
|
|
|------|----------|------------------|
|
|||
|
|
| `navigate(path)` | 程序化跳转 | ✅ 有 history entry |
|
|||
|
|
| `<Link to="/">Home</Link>` | 声明式导航 | ✅ 预加载 prefetch |
|
|||
|
|
| `navigate(-1)` | 返回上一页 | ✅ |
|
|||
|
|
| `<Navigate to="/login" replace />` | 替换当前 entry | ❌ 不增加 history |
|
|||
|
|
|
|||
|
|
## 关联笔记
|