452 lines
16 KiB
Markdown
452 lines
16 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, Router, Navigation, Frontend]
|
|||
|
|
create time: 2026-04-29 22:07
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 路由管理
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
前端路由决定了"URL 变化 → 组件渲染"的映射关系。在单页应用(SPA)中,浏览器**不会重新加载整页**,而是由路由库拦截 URL 变更、动态切换组件——这也就是为什么 SPA 体验如此流畅。
|
|||
|
|
|
|||
|
|
> [!question] 思考:浏览器刷新和客户端跳转的本质区别
|
|||
|
|
> - **刷新**:向服务器发起完整 HTTP 请求,拿到新 HTML → 重建整个 DOM
|
|||
|
|
> - **客户端跳转**:只改变 URL + 执行 JS 渲染对应组件,无需请求页面资源
|
|||
|
|
>
|
|||
|
|
> 理解这个区别是掌握前端路由的前提。
|
|||
|
|
|
|||
|
|
本文档以 React Router v7 为主线,覆盖路由配置、嵌套路由、懒加载、数据加载、守卫拦截等核心模式。同时补充 TanStack Router 作为类型安全方案的对比。
|
|||
|
|
|
|||
|
|
## React Router v7 核心概念
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
Start["URL 变更"] --> Match["路由匹配<br/>按 path 从上到下匹配"]
|
|||
|
|
Match --> BestMatch["最佳匹配胜出"]
|
|||
|
|
BestMatch --> Render["渲染组件树<br/>Layout Route + Child Routes"]
|
|||
|
|
Render --> Outlet["Outlet 占位"]
|
|||
|
|
Outlet --> Content["具体页面组件"]
|
|||
|
|
|
|||
|
|
Sub1["声明式: Link"] -.-> Start
|
|||
|
|
Sub2["程序化: navigate()"] -.-> Start
|
|||
|
|
|
|||
|
|
subgraph Config["路由配置 Route Tree"]
|
|||
|
|
Root["path / RootLayout"]
|
|||
|
|
Root --> Index["index true HomePage"]
|
|||
|
|
Root --> About["path about AboutPage"]
|
|||
|
|
Root --> Dash["path dashboard DashboardLayout"]
|
|||
|
|
Dash --> Stats["path stats StatsPage"]
|
|||
|
|
Dash --> Settings["path settings SettingsPage"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
Render --> Config
|
|||
|
|
|
|||
|
|
style Start fill:#F4DBD6,color:#000
|
|||
|
|
style Render fill:#61DAFB,color:#000
|
|||
|
|
style Config fill:#E8E8E8,color:#000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### SPA 路由的工作流程
|
|||
|
|
|
|||
|
|
`浏览器操作` → `拦截 URL 变化` → `匹配 Route 树` → `渲染对应组件`
|
|||
|
|
|
|||
|
|
两条跳转路径:
|
|||
|
|
- **声明式导航**:用户点击 `<Link>` 组件,浏览器地址栏更新但不刷新
|
|||
|
|
- **程序化导航**:JS 调用 `navigate("/path")`,常用于登录后跳转、表单提交后重定向
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## React Router vs TanStack Router 选型
|
|||
|
|
|
|||
|
|
| 维度 | React Router (v7) | TanStack Router |
|
|||
|
|
|------|-------------------|-----------------|
|
|||
|
|
| Bundle 大小 | ~25KB | ~20KB |
|
|||
|
|
| TypeScript 支持 | 需手动写类型 | 代码生成,全链路类型安全 |
|
|||
|
|
| Data APIs | loader / action(v7)| loader / action(更成熟)|
|
|||
|
|
| 学习曲线 | 低(熟悉 React 即懂) | 中(有自身 API 体系)|
|
|||
|
|
| 适用场景 | 大多数项目 | TS 重度项目、大型应用 |
|
|||
|
|
|
|||
|
|
> [!tip] 选型建议
|
|||
|
|
> - 中小型项目或团队 TS 经验一般 → **React Router**,上手快、文档丰富
|
|||
|
|
> - 大型企业级项目、强依赖类型安全 → **TanStack Router**,编译期捕获路由错误
|
|||
|
|
|
|||
|
|
### 基本结构
|
|||
|
|
|
|||
|
|
React Router v7 使用扁平化的 Route Tree,父子关系通过 `children` 数组表达。核心概念:**Layout Route**(不切换内容的父布局)和 **Page Route**(实际渲染页面的叶子节点)。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// React Router DOM v7
|
|||
|
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|||
|
|
|
|||
|
|
const router = createBrowserRouter([
|
|||
|
|
{
|
|||
|
|
path: "/",
|
|||
|
|
element: <RootLayout />, // 所有页面共用的外层 shell
|
|||
|
|
children: [
|
|||
|
|
{ index: true, element: <HomePage /> }, // "/" → 首页
|
|||
|
|
{ path: "about", element: <AboutPage /> }, // "/about"
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
function App() {
|
|||
|
|
return <RouterProvider router={router} />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] Index Route vs Path Route
|
|||
|
|
> - `index: true`:当 parent route 被匹配且**没有更深的子路径**时激活。例如 `/dashboard` 命中 Layout 但没带子路径 → index route 显示。
|
|||
|
|
> - `path: "xxx"`:当 URL 带有该段时才激活。例如 `/dashboard/stats` → stats route。
|
|||
|
|
|
|||
|
|
## Layout Route 与嵌套路由
|
|||
|
|
|
|||
|
|
Layout Route 是一种**不随 URL 变化而卸载**的父级路由。它的核心作用是让侧边栏、顶部导航等共用 UI 在页面切换时保持存活,避免重复初始化。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
{
|
|||
|
|
// /dashboard 及其所有子路径共用此布局
|
|||
|
|
path: "dashboard",
|
|||
|
|
element: <DashboardLayout />, // 侧边栏 + Header 常驻不卸载
|
|||
|
|
children: [
|
|||
|
|
{ index: true, element: <DashboardHome /> }, // /dashboard → DashboardHome
|
|||
|
|
{ path: "stats", element: <StatsPage /> }, // /dashboard/stats → StatsPage
|
|||
|
|
{ path: "settings", element: <SettingsPage /> }, // /dashboard/settings → SettingsPage
|
|||
|
|
{ path: "settings/:tab", element: <SettingsTab /> }, // /dashboard/settings/profile
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// DashboardLayout.tsx
|
|||
|
|
import { Outlet } from "react-router-dom";
|
|||
|
|
|
|||
|
|
function DashboardLayout() {
|
|||
|
|
return (
|
|||
|
|
<div className="dashboard-layout">
|
|||
|
|
<Sidebar />
|
|||
|
|
<header><Navigation /></header>
|
|||
|
|
<main>
|
|||
|
|
{/* <Outlet /> 是嵌套内容的"占位槽" */}
|
|||
|
|
<Outlet />
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] <Outlet /> 的本质
|
|||
|
|
> `<Outlet />` 是一个**占位组件**。当用户访问 `/dashboard/stats` 时:
|
|||
|
|
> 1. React Router 匹配到 `path: "dashboard"` 这个 layout route,渲染 `<DashboardLayout />`
|
|||
|
|
> 2. 在 `<DashboardLayout />` 内部遇到 `<Outlet />` → 继续向下匹配子路由
|
|||
|
|
> 3. 子路由 `<StatsPage />` 被渲染到 `<Outlet />` 的位置
|
|||
|
|
>
|
|||
|
|
> **关键理解**:没有 `<Outlet />`,子路由内容就无法显示!这是 v6/v7 中嵌套路由的唯一通信方式。
|
|||
|
|
|
|||
|
|
> [!warning] 常见坑:忘记加 Outlet
|
|||
|
|
> ```tsx
|
|||
|
|
> // ❌ 错误:layout route 的子路由永远不会渲染
|
|||
|
|
> function DashboardLayout() {
|
|||
|
|
> return <><Sidebar /><StatsPage /></>; // 硬编码了具体页面
|
|||
|
|
> }
|
|||
|
|
>
|
|||
|
|
> // ✅ 正确:用 Outlet 留出插槽
|
|||
|
|
> function DashboardLayout() {
|
|||
|
|
> return <><Sidebar /><Outlet /></>;
|
|||
|
|
> }
|
|||
|
|
> ```
|
|||
|
|
|
|||
|
|
## 路由参数获取
|
|||
|
|
|
|||
|
|
React Router 提供了一组 Hook,让我们可以在组件中读取 URL 信息:
|
|||
|
|
|
|||
|
|
```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, search, hash }
|
|||
|
|
const navigate = useNavigate(); // 编程式导航
|
|||
|
|
|
|||
|
|
const query = searchParams.get("q"); // "hello"
|
|||
|
|
console.log(location.pathname); // "/user/123"
|
|||
|
|
|
|||
|
|
return <h1>User #{id}, searching for "{query}"</h1>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Hook 速查表
|
|||
|
|
|
|||
|
|
| Hook | 返回 | 典型用法 |
|
|||
|
|
|------|------|----------|
|
|||
|
|
| `useParams()` | `{ param: string }` | 从 URL 段中提取动态值(如 `/:id`) |
|
|||
|
|
| `useSearchParams()` | `[URLSearchParams, func]` | 读写查询字符串(如 `?q=xxx`) |
|
|||
|
|
| `useLocation()` | `{ pathname, search, hash }` | 当前 URL 各部分的细粒度访问 |
|
|||
|
|
| `useNavigate()` | `navigate(path, options?)` | 编程式跳转、后退、替换 history entry |
|
|||
|
|
|
|||
|
|
> [!tip] searchParams vs Location.search
|
|||
|
|
> - `searchParams.get("key")` 直接得到解码后的字符串,方便操作
|
|||
|
|
> - `location.search` 是原始字符串 `"?q=hello"`,需要手动解析
|
|||
|
|
> - 推荐优先使用 `useSearchParams()`,类型更安全。
|
|||
|
|
|
|||
|
|
### v7 Data Loader —— 数据预加载
|
|||
|
|
|
|||
|
|
v7 引入了 `loader` 和 `action`,让数据获取成为路由级别的一等公民:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// Route 定义
|
|||
|
|
{
|
|||
|
|
path: "posts/:postId",
|
|||
|
|
// loader 在组件渲染前执行,返回的数据自动注入 component props
|
|||
|
|
loader: async ({ params }) => {
|
|||
|
|
const res = await fetch(`/api/posts/${params.postId}`);
|
|||
|
|
if (!res.ok) throw new Error("Post not found");
|
|||
|
|
return res.json(); // 返回的对象将作为 props 传给组件
|
|||
|
|
},
|
|||
|
|
action: async ({ request, params }) => {
|
|||
|
|
const formData = await request.formData();
|
|||
|
|
const content = formData.get("content") as string;
|
|||
|
|
await fetch(`/api/posts/${params.postId}`, {
|
|||
|
|
method: "PATCH",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ content }),
|
|||
|
|
});
|
|||
|
|
return { success: true }; // action 返回值可通过 useActionData() 读取
|
|||
|
|
},
|
|||
|
|
// errorElement 处理 loader/action 中抛出的错误
|
|||
|
|
errorElement: <PostErrorBoundary />,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// PostPage.tsx —— 消费 loader 数据
|
|||
|
|
import { useLoaderData, useActionData } from "react-router-dom";
|
|||
|
|
|
|||
|
|
function PostPage() {
|
|||
|
|
const post = useLoaderData() as { title: string; body: string }; // loader 的返回值
|
|||
|
|
const actionResult = useActionData(); // action 的返回值
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<h1>{post.title}</h1>
|
|||
|
|
<p>{post.body}</p>
|
|||
|
|
{actionResult?.success && <div className="toast">保存成功!</div>}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!question] loader vs useEffect 获取数据有什么区别?
|
|||
|
|
> - **loader**:先取数据 → 数据就绪后 → 才渲染页面。用户不会看到空状态或 loading 骨架屏(配合 Suspense 可以显示 fallback)。
|
|||
|
|
> - **useEffect + useState**:先渲染空白/loading → 请求发出 → 数据回来 → 更新 UI。会有闪烁过程。
|
|||
|
|
>
|
|||
|
|
> loader 本质是**服务端渲染(SSR)友好的异步数据模式**。即使不使用 SSR,它的"数据先于渲染"语义也能减少水合闪烁。
|
|||
|
|
|
|||
|
|
## 懒加载 Code Splitting
|
|||
|
|
|
|||
|
|
代码分割的核心目标:**减少首屏加载体积**。通过 `React.lazy()` + `<Suspense>`,将非关键路由的代码拆成独立 chunk,用户访问时才下载。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { lazy, Suspense } from "react";
|
|||
|
|
|
|||
|
|
// 路由级代码分割 —— 只在组件真正需要时才加载
|
|||
|
|
const AdminPage = lazy(() => import("./pages/AdminPage"));
|
|||
|
|
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
|||
|
|
|
|||
|
|
// Route 配置中包裹 Suspense
|
|||
|
|
{
|
|||
|
|
path: "admin",
|
|||
|
|
element: (
|
|||
|
|
<Suspense fallback={<PageSpinner />}>
|
|||
|
|
<AdminPage />
|
|||
|
|
</Suspense>
|
|||
|
|
),
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 更优方案:loader + Suspense(v7 Data Router)
|
|||
|
|
|
|||
|
|
当 `loader` 和 `lazy()` 结合使用时,可以在数据加载之前就展示 fallback,实现"Loading → Fallback → Page"的自然过渡:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const ExplorePage = lazy(
|
|||
|
|
() => import("./pages/ExplorePage"),
|
|||
|
|
{ ssr: false } // v7 可选:标记为非 SSR 页面
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
path: "explore",
|
|||
|
|
loader: async () => {
|
|||
|
|
// 1. 先执行数据请求(此时显示 fallback)
|
|||
|
|
const categories = await fetchCategories();
|
|||
|
|
return { categories };
|
|||
|
|
},
|
|||
|
|
element: (
|
|||
|
|
<Suspense fallback={<ExploreFallback />}>
|
|||
|
|
{/* 2. chunk 加载完毕 + loader 数据就绪后渲染 */}
|
|||
|
|
<ExplorePage />
|
|||
|
|
</Suspense>
|
|||
|
|
),
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 懒加载时机判断
|
|||
|
|
> - **不要懒加载**:首页、登录页等高频入口
|
|||
|
|
> - **适合懒加载**:设置页、管理员面板、低频功能模块
|
|||
|
|
> - **chunk 大小建议**:每个 chunk gzipped 后不超过 100KB
|
|||
|
|
> - **公共代码尽量提取**:避免多个 chunk 重复包含 React / utils
|
|||
|
|
|
|||
|
|
## 动态路由与 Splat 路由
|
|||
|
|
|
|||
|
|
URL 中的可变部分是前端路由的核心灵活性来源。React Router 提供了两种参数匹配方式:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// :param —— 匹配单个路径段
|
|||
|
|
{ path: "users/:userId", element: <UserProfile /> } // /users/42 ✅
|
|||
|
|
// /users/42/posts ❌(不匹配,需子路由)
|
|||
|
|
|
|||
|
|
// * splat —— 贪婪匹配剩余所有段
|
|||
|
|
{ path: "docs/*", element: <DocViewer /> } // /docs/a/b/c ✅
|
|||
|
|
{ path: "*", element: <NotFoundPage /> } // /anything ❌ → 404 fallback
|
|||
|
|
|
|||
|
|
// 多参数组合
|
|||
|
|
{
|
|||
|
|
path: "posts/:postId/comments/:commentId",
|
|||
|
|
element: <CommentDetail />, // /posts/1/comments/5
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Error Route —— 错误边界处理
|
|||
|
|
|
|||
|
|
v7 中每个路由都可以声明 `errorElement`,当 loader/action 抛出异常时自动渲染:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
{
|
|||
|
|
path: "admin",
|
|||
|
|
loader: adminLoader, // 可能抛 401/403
|
|||
|
|
errorElement: <AdminErrorBoundary />, // 接管错误 UI
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminErrorBoundary.tsx
|
|||
|
|
import { useRouteError } from "react-router-dom";
|
|||
|
|
|
|||
|
|
function AdminErrorBoundary() {
|
|||
|
|
const error = useRouteError() as Response;
|
|||
|
|
|
|||
|
|
if (error.status === 401) return <Navigate to="/login" replace />;
|
|||
|
|
if (error.status === 403) return <UnauthorizedPage />;
|
|||
|
|
|
|||
|
|
return <GenericErrorPage message={error.data?.message ?? "加载失败"} />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!warning] 全局 404 位置很重要
|
|||
|
|
> `{ path: "*" }` 必须放在路由列表**最末尾**。React Router 从上到下匹配,一旦命中即停止。如果提前放 `*`,后续所有路由都不会生效。
|
|||
|
|
|
|||
|
|
## Form Actions —— 数据变更流程
|
|||
|
|
|
|||
|
|
`action` 和 `loader` 是 v7 Data Router 的对称设计:`loader` 负责读取(GET),`action` 负责写入(POST/PATCH/DELETE)。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
{
|
|||
|
|
path: "posts/:postId",
|
|||
|
|
action: async ({ params, request }) => {
|
|||
|
|
const formData = await request.formData();
|
|||
|
|
const title = formData.get("title") as string;
|
|||
|
|
|
|||
|
|
await fetch(`/api/posts/${params.postId}`, {
|
|||
|
|
method: "PATCH",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ title }),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 提交后重定向到详情页(PRG 模式,避免重复提交)
|
|||
|
|
return new Response(null, {
|
|||
|
|
status: 302,
|
|||
|
|
headers: { Location: `/posts/${params.postId}` },
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] PRG 模式(Post/Redirect/Get)
|
|||
|
|
> action 执行完毕后**必须 redirect**,否则用户刷新页面时会重复提交表单。这是后端开发的老经验,同样适用于 SPA 的 Data Router 场景。
|
|||
|
|
|
|||
|
|
## 路由守卫与权限控制
|
|||
|
|
|
|||
|
|
权限校验的演进路线:**客户端条件渲染 → Loader 前置检查**。推荐优先使用 loader guard,因为它在服务端渲染和预取场景下也能正确拦截。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// ❌ 方案1:条件渲染(简单但不够优雅)
|
|||
|
|
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|||
|
|
const { user } = useAuth();
|
|||
|
|
if (!user) return <Navigate to="/login" replace />;
|
|||
|
|
return <>{children}</>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ❌ 方案2:HOC 包装
|
|||
|
|
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 Guard(推荐,可配合 errorElement 统一错误处理)
|
|||
|
|
{
|
|||
|
|
path: "admin",
|
|||
|
|
loader: async () => {
|
|||
|
|
const res = await fetch("/api/me");
|
|||
|
|
if (!res.ok) throw new Response("未登录", { status: 401 });
|
|||
|
|
const user = await res.json();
|
|||
|
|
if (!user.isAdmin) throw new Response("权限不足", { status: 403 });
|
|||
|
|
return user; // 用户信息注入 component props
|
|||
|
|
},
|
|||
|
|
element: <AdminPanel />,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 多级权限设计建议
|
|||
|
|
|
|||
|
|
| 层级 | 方式 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| URL 层 | `errorElement` + loader | 拦截非法访问,返回 401/403 |
|
|||
|
|
| UI 层 | `useLoaderData()` 判断 | 基于用户角色隐藏按钮/菜单项 |
|
|||
|
|
| API 层 | 后端 JWT / Session | **最终防线**,前端判断必须被后端验证 |
|
|||
|
|
|
|||
|
|
> [!warning] 前端权限 ≠ 安全保障
|
|||
|
|
> 所有前端守卫只影响用户体验,真正的安全必须由后端 API 保障。不要把敏感逻辑放在前端 loader 中。
|
|||
|
|
|
|||
|
|
## 导航 API 对比与选择
|
|||
|
|
|
|||
|
|
| 方式 | 代码示例 | 适用场景 | 保留历史 | Prefetch |
|
|||
|
|
|------|----------|----------|---------|----------|
|
|||
|
|
| `<Link>` | `<Link to="/about">` | 页面内跳转(推荐首选) | ✅ | 可配置 prefetch |
|
|||
|
|
| `navigate()` | `navigate("/home")` | JS 逻辑中触发跳转 | ✅ | ❌ |
|
|||
|
|
| `navigate(-1)` | — | "返回上一页"按钮 | ✅ | N/A |
|
|||
|
|
| `<Navigate replace />` | `<Navigate to="/login" replace />` | 登录后自动重定向,不污染 history | ❌ | N/A |
|
|||
|
|
| `router.navigate()` | TanStack Router 写法 | TS 项目、类型安全导航 | ✅ | 内置 |
|
|||
|
|
|
|||
|
|
### History Entry 管理技巧
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
|
|||
|
|
// 登录成功后替换当前 entry,用户点后退不会回到登录页
|
|||
|
|
navigate("/dashboard", { replace: true });
|
|||
|
|
|
|||
|
|
// 弹窗关闭后回到原来的位置
|
|||
|
|
navigate(-1);
|
|||
|
|
|
|||
|
|
// 带 state 跳转(常用于传递临时状态)
|
|||
|
|
navigate("/settings", { state: { fromTab: "security" } });
|
|||
|
|
// 子页面通过 useLocation().state 读取
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/REACT/3. 生态工具篇/09-状态管理]] — 路由守卫中获取用户信息需配合状态管理或 Context
|
|||
|
|
- [[hhs/REACT/3. 生态工具篇/10-TS + React]] — TanStack Router 的全链路类型安全依赖完善的 TS 基础
|