React Router

为什么需要路由?

单页应用(SPA):在单页面中实现多视图切换,避免整页刷新。

核心功能

  • 根据 URL 路径渲染对应组件。

  • 实现页面间导航(前进、后退、跳转)。

  • 支持动态路由、嵌套路由、路由守卫等。


一、RouterProvider 核心概念

1. 定位与作用

  • React Router v6.4+ 新增特性:用于替代传统的 <BrowserRouter>,提供 数据驱动路由(Data Routing)能力。

  • 核心能力

    • 集中式路由配置:路由定义与组件解耦。

    • 数据预加载:通过 loader 在路由匹配时自动加载数据。

    • 表单处理:通过 action 处理表单提交。

    • 内置优化:流式渲染(Suspense)、错误边界、滚动恢复等。

  • 适用场景:中大型项目、需复杂数据流管理、服务端渲染(SSR)准备。


二、基础配置与使用

1. 安装依赖

npm install react-router-dom

2. 创建路由配置

// src/router.js
import { createBrowserRouter } from "react-router-dom";
import App from "./App";
import Home from "./pages/Home";
import UserProfile, { loader as userLoader } from "./pages/UserProfile";

// 定义路由配置对象
export const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    children: [
      { index: true, element: <Home /> },
      {
        path: "users/:id",
        element: <UserProfile />,
        loader: userLoader, // 数据预加载
        errorElement: <ErrorPage />, // 错误边界
      },
    ],
  },
]);

3. 在入口文件中使用

// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

三、数据流管理(loader 与 action

1. loader:路由数据预加载

  • 触发时机:路由匹配时自动调用,在组件渲染前完成数据加载。

  • 典型场景:API 请求、权限校验、数据初始化。

// src/pages/UserProfile.jsx
export async function loader({ params, request }) {
  // 获取动态参数(如用户ID)
  const userId = params.id;
  
  // 发起数据请求
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new Error("用户不存在"); // 抛出错误会被 errorElement 捕获
  }
  return response.json();
}

function UserProfile() {
  // 使用 useLoaderData 获取 loader 返回的数据
  const userData = useLoaderData();
  return <div>{userData.name}</div>;
}

2. action:处理表单提交

  • 触发时机:表单提交时自动调用(需设置 method="post")。

  • 典型场景:用户注册、数据修改、文件上传。

// src/pages/EditUser.jsx
export async function action({ request, params }) {
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  
  // 提交数据到后端
  await fetch(`/api/users/${params.id}`, {
    method: "PUT",
    body: JSON.stringify(updates),
  });
  
  // 重定向到用户详情页
  return redirect(`/users/${params.id}`);
}

function EditUser() {
  return (
    <Form method="post">
      <input name="name" type="text" />
      <button type="submit">保存</button>
    </Form>
  );
}

四、高级功能与最佳实践

1. 错误处理与边界

  • 全局错误捕获:通过路由配置的 errorElement 统一处理。

  • 局部错误处理:在组件内使用 useRouteError Hook。

// 全局错误页面配置
{
  path: "/users/:id",
  element: <UserProfile />,
  loader: userLoader,
  errorElement: <ErrorBoundary />, // 自定义错误组件
}

// ErrorBoundary.jsx
import { useRouteError } from "react-router-dom";

function ErrorBoundary() {
  const error = useRouteError();
  return (
    <div>
      <h1>出错了!</h1>
      <p>{error.message}</p>
    </div>
  );
}

2. 路由懒加载与代码分割

// 结合 React.lazy 和 Suspense 实现懒加载
import { lazy, Suspense } from "react";

const Settings = lazy(() => import("./pages/Settings"));

const router = createBrowserRouter([
  {
    path: "/settings",
    element: (
      <Suspense fallback={<div>加载中...</div>}>
        <Settings />
      </Suspense>
    ),
  },
]);

3. 权限控制与路由守卫

// 封装鉴权高阶路由配置
function protectedLoader({ request }) {
  const isAuthenticated = checkAuth();
  if (!isAuthenticated) {
    return redirect("/login");
  }
  return null;
}

const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: <Dashboard />,
    loader: protectedLoader, // 路由加载前鉴权
  },
]);

4. 滚动恢复与定位

  • 自动恢复:React Router 默认记录滚动位置。

  • 手动控制:使用 <ScrollRestoration> 组件。

import { ScrollRestoration } from "react-router-dom";

function App() {
  return (
    <div>
      <ScrollRestoration />
      {/* 其他内容 */}
    </div>
  );
}

五、与传统 BrowserRouter 对比

功能RouterProviderBrowserRouter
路由配置集中式(JSON 结构)分散式(组件树中声明)
数据加载内置 loader,自动预加载需手动使用 useEffect
表单处理内置 action,统一管理提交逻辑需自行处理表单事件和状态
错误处理全局/局部错误边界,自动捕获需手动实现错误边界
代码分割天然支持 Suspense + 懒加载需手动配置
服务端渲染更友好(数据预加载机制)需额外配置

方案一:使用 createBrowserRouter + RouterProvider(推荐)

// router.js
import { createBrowserRouter } from "react-router-dom"
import App from "./App"
import Home from "./pages/home"
import News from "./pages/news"

export default createBrowserRouter([
  {
    path: '/',
    element: <App />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: '/news',
        element: <News />
      }
    ]
  }
])

// App.jsx
import './app.css'
import { Link, Outlet } from 'react-router-dom'

function App() {
  return (
    <div className='main'>
      <nav>
        <Link to="/">Home</Link> {/* 修改为根路径 */}
        <Link to="/news">News</Link>
      </nav>

      <div className="content">
        <Outlet />
      </div>
    </div>
  )
}

// main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router/router'
import { Provider } from 'react-redux'
import store from './store/index.js'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <Provider store={store}>
      <RouterProvider router={router} />
    </Provider>
  </StrictMode>
)

方案二:使用 BrowserRouter 组件方式

// main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import store from './store/index.js'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </StrictMode>
)

// App.jsx
import './app.css'
import { Link, Outlet, Routes, Route } from 'react-router-dom'
import Home from './pages/home'
import News from './pages/news'

function App() {
  return (
    <div className='main'>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/news">News</Link>
      </nav>

      <div className="content">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/news" element={<News />} />
        </Routes>
      </div>
    </div>
  )
}

六、常见问题与解决方案

1. 如何传递自定义参数到 loader

  • 方案:通过 URL 参数或搜索参数传递,或在跳转时携带状态:

    navigate("/users/123", { state: { from: "home" } });
    
    // loader 中获取
    export async function loader({ request }) {
      const url = new URL(request.url);
      const from = url.searchParams.get("from");
      // 或通过 location.state
      const state = useLocation().state;
    }

2. 如何复用 loader 逻辑?

  • 方案:封装为共享函数:

    // utils/loaders.js
    export async function loadUserData(userId) {
      const res = await fetch(`/api/users/${userId}`);
      return res.json();
    }
    
    // 路由配置
    import { loadUserData } from "./utils/loaders";
    loader: ({ params }) => loadUserData(params.id)

3. 如何结合 TypeScript 使用?

  • 类型定义:为 loader/action 定义类型:

    import { LoaderFunctionArgs, ActionFunctionArgs } from "react-router-dom";
    
    export async function loader({ params }: LoaderFunctionArgs) {
      // params 自动推断为 { id: string }
    }

七、总结

何时选择 RouterProvider

  • 大型应用:需要集中管理路由和数据流。

  • 复杂数据依赖:多路由共享数据、预加载需求。

  • 优化需求:流式渲染、代码分割、滚动恢复。

  • 未来扩展:计划迁移到服务端渲染(SSR)。

最佳实践

  1. 路由分层管理:拆分为多个路由配置文件。

  2. 严格类型定义:结合 TypeScript 提升安全性。

  3. 合理拆分组件:保持路由配置简洁清晰。

  4. 性能监控:结合 Suspense 和懒加载优化首屏速度。


 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值