React 路由
1. 客户端路由
在传统的网站(多页面应用,MPA)中,每次点击一个链接,浏览器都会向服务器发送一个新请求,然后服务器返回一个全新的 HTML 页面,导致整个页面刷新。
而在客户端路由(单页面应用,SPA)中,导航行为由前端的 JavaScript 代码接管。当用户点击链接时:
- JavaScript 会阻止浏览器的默认跳转行为。
- 它会手动更新浏览器的 URL(使用 History API),但不会刷新页面。
- React Router 会根据新的 URL,在当前页面上卸载旧的组件,挂载新的组件,从而实现视图的切换。
这种方式带来了如 App 般流畅的无刷新体验。
React Router 是 React 生态中最流行、功能最强大的路由库。它能让我们以声明式的方式将 URL 与组件进行映射,并提供了嵌套路由、数据加载、过渡动画等一系列强大的功能。
2. 基础设置与路由定义
基础设置
现代 React Router 推荐使用基于对象的 API 来配置路由。使用如下命令安装 react-router-dom
:
npm install react-router-dom
我们主要使用两个核心 API:
createBrowserRouter
: 用于创建一个路由配置实例。RouterProvider
: 一个组件,用于将你的路由配置提供给整个应用。
创建路由配置
最佳实践是将路由配置抽离到一个单独的文件中(例如 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;
渲染路由
在应用入口文件(通常是 main.jsx
)中,使用 createBrowserRouter
和 RouterProvider
来激活路由。
// 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. 导航
声明式导航: <Link>
与 <NavLink>
为了实现客户端路由,必须使用 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>
命令式导航: 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 最强大的功能之一,它让我们可以轻松创建共享布局的页面(如带有侧边栏和顶栏的仪表盘)。
使用 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. 动态路由与参数
动态段
通过在路径中使用冒号 (:
),可以定义一个动态段 (Dynamic Segments),它会匹配 URL 中的任意值。
// path: '/users/:userId'
// 这个路径可以匹配 /users/123, /users/abc 等
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
基本概念
loader
是一个革命性的功能,它让我们可以在组件渲染前声明式地加载数据。
- 在路由配置中添加一个
loader
函数。 - 该函数可以执行 API 请求并返回数据。
- 在组件中使用
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>;
}
这种模式极大地简化了数据获取、加载中和错误状态的管理。
受保护的路由
实现路由保护(例如,用户必须登录才能访问)的最佳方式是结合 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();
});