Dark Dwarf Blog background

React 路由

React 路由

1. 客户端路由

在传统的网站(多页面应用,MPA)中,每次点击一个链接,浏览器都会向服务器发送一个新请求,然后服务器返回一个全新的 HTML 页面,导致整个页面刷新

而在客户端路由(单页面应用,SPA)中,导航行为由前端的 JavaScript 代码接管。当用户点击链接时:

  1. JavaScript 会阻止浏览器的默认跳转行为。
  2. 它会手动更新浏览器的 URL(使用 History API),但不会刷新页面。
  3. React Router 会根据新的 URL,在当前页面上卸载旧的组件,挂载新的组件,从而实现视图的切换。

这种方式带来了如 App 般流畅的无刷新体验。

React Router 是 React 生态中最流行、功能最强大的路由库。它能让我们以声明式的方式将 URL 与组件进行映射,并提供了嵌套路由、数据加载、过渡动画等一系列强大的功能。

2. 基础设置与路由定义

a.a. 基础设置

现代 React Router 推荐使用基于对象的 API 来配置路由。使用如下命令安装 react-router-dom

npm install react-router-dom

我们主要使用两个核心 API:

  • createBrowserRouter: 用于创建一个路由配置实例。
  • RouterProvider: 一个组件,用于将你的路由配置提供给整个应用。

b.b. 创建路由配置

最佳实践是将路由配置抽离到一个单独的文件中(例如 routes.jsx)。

// routes.jsx
import App from "./App";
import Profile from "./Profile";
import ErrorPage from "./ErrorPage";

const routes = [
  {
    path: "/",
    element: <App />,
    errorElement: <ErrorPage /> // 当此路径下发生错误时渲染的组件
  },
  {
    path: "/profile",
    element: <Profile />,
  },
];

export default routes;

c.c. 渲染路由

在应用入口文件(通常是 main.jsx)中,使用 createBrowserRouterRouterProvider 来激活路由。

// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import routes from './routes';

// 1. 基于配置创建路由实例
const router = createBrowserRouter(routes);

// 2. 使用 RouterProvider 渲染
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

3. 导航

为了实现客户端路由,必须使用 React Router 提供的 <Link> 组件来替代原生的 <a> 标签。

  • <Link to="/profile">Profile</Link>: 它会渲染一个 <a> 标签,但会阻止其默认的页面刷新行为,并由路由接管导航。
  • <NavLink>: 是 <Link> 的一个特殊版本。当它指向的 URL 与当前页面 URL 匹配时,它可以自动添加一个 active class 或应用 style 属性,非常适合用于导航菜单的高亮显示。
import { NavLink } from 'react-router-dom';

<NavLink 
  to="/messages"
  style={({ isActive }) => isActive ? { color: 'red' } : undefined}
>
  Messages
</NavLink>

b.b. 命令式导航: useNavigate Hook

在某些场景下(如表单提交成功后),我们需要通过代码来跳转页面。useNavigate Hook 可以很便捷地实现这个功能。

import { useNavigate } from 'react-router-dom';

function MyForm() {
  const navigate = useNavigate();

  const handleSubmit = (event) => {
    event.preventDefault();
    // ... 提交逻辑
    navigate('/success'); // 跳转到成功页面
  };

  return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

4. 嵌套路由与布局

嵌套路由是 React Router 最强大的功能之一,它让我们可以轻松创建共享布局的页面(如带有侧边栏和顶栏的仪表盘)。

a.a. 使用 children 属性和 <Outlet>

通过在路由配置中使用 children 数组,并在父组件中使用 <Outlet /> 组件,可以实现路由嵌套。

// routes.jsx
const routes = [
  {
    path: "/dashboard",
    element: <DashboardLayout />,
    children: [
      { index: true, element: <DashboardHome /> }, // 默认子路由
      { path: "invoices", element: <Invoices /> },
      { path: "team", element: <Team /> },
    ]
  }
];

// DashboardLayout.jsx
import { Outlet } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div>
      <Sidebar />
      <main>
        {/* 子路由组件将在这里被渲染 */}
        <Outlet />
      </main>
    </div>
  );
}
  • 当用户访问 /dashboard 时,<Outlet /> 会渲染 <DashboardHome />
  • 当用户访问 /dashboard/invoices 时,<Outlet /> 会渲染 <Invoices />

5. 动态路由与参数

a.a. 动态段

通过在路径中使用冒号 (:),可以定义一个动态段 (Dynamic Segments),它会匹配 URL 中的任意值。

// path: '/users/:userId'
// 这个路径可以匹配 /users/123, /users/abc 等

b.b. useParams Hook

在匹配了动态路由的组件中,可以使用 useParams Hook 来获取 URL 中的动态参数值。

// 在匹配 '/users/:userId' 的组件中
import { useParams } from 'react-router-dom';

function UserProfile() {
  const { userId } = useParams(); // { userId: '123' }
  // ... 根据 userId 获取用户数据
  return <h1>Profile for User ID: {userId}</h1>;
}

6. 错误处理

现代 React Router 提供了强大的错误处理机制。通过在路由配置中提供 errorElement,我们可以优雅地捕获和处理渲染、加载数据或执行操作时发生的错误。

// routes.jsx
{
  path: "/",
  element: <App />,
  // 当 App 或其子路由发生错误时,会渲染 ErrorPage
  errorElement: <ErrorPage />,
}

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

function ErrorPage() {
  const error = useRouteError(); // 获取错误信息
  console.error(error);
  return <div>Oops! An error occurred: {error.statusText || error.message}</div>;
}

7. Loader

a.a. 基本概念

loader 是一个革命性的功能,它让我们可以在组件渲染前声明式地加载数据。

  1. 在路由配置中添加一个 loader 函数。
  2. 该函数可以执行 API 请求并返回数据。
  3. 在组件中使用 useLoaderData Hook 来访问加载好的数据。
// routes.jsx
{
  path: '/users/:userId',
  element: <UserProfile />,
  loader: async ({ params }) => {
    const response = await fetch(`/api/users/${params.userId}`);
    return response.json();
  },
}

// UserProfile.jsx
import { useLoaderData } from 'react-router-dom';

function UserProfile() {
  const user = useLoaderData(); // 直接获取 loader 返回的数据
  return <h1>{user.name}</h1>;
}

这种模式极大地简化了数据获取、加载中和错误状态的管理。

b.b. 受保护的路由

实现路由保护(例如,用户必须登录才能访问)的最佳方式是结合 loader

// routes.jsx
import { redirect } from "react-router-dom";

{
  path: '/dashboard',
  element: <Dashboard />,
  loader: async () => {
    const isLoggedIn = await checkAuth(); // 检查用户登录状态的函数
    if (!isLoggedIn) {
      // 如果未登录,重定向到登录页
      return redirect('/login');
    }
    return null; // 已登录,允许渲染
  },
}

这种方法非常健壮,因为它在组件渲染之前就完成了验证和重定向,避免了受保护内容的瞬间闪烁。

8. 在测试中使用路由

由于组件中可能包含 <Link> 或使用 useNavigate 等 Hooks,直接在测试中渲染它们会报错,我们需要提供一个路由上下文。

对于需要测试完整导航逻辑的集成测试,最佳方法是使用 createMemoryRouter。它在内存中模拟路由行为,非常适合测试环境。

import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import routes from './routes'; // 导入你的应用路由配置

it('navigates from home to profile', async () => {
  const user = userEvent.setup();
  // 1. 创建一个内存路由,并指定初始路径
  const router = createMemoryRouter(routes, {
    initialEntries: ['/'],
  });

  // 2. 使用 RouterProvider 渲染
  render(<RouterProvider router={router} />);

  // 3. 模拟用户点击链接
  await user.click(screen.getByRole('link', { name: /profile/i }));

  // 4. 断言页面已跳转
  expect(screen.getByRole('heading', { name: /hello from profile/i })).toBeInTheDocument();
});