React Router 原理与使用
问题
React Router 的工作原理是什么?如何在 React 应用中实现路由功能?
答案
React Router 是 React 生态中最流行的路由库,用于在单页应用(SPA)中实现客户端路由。
核心概念
路由模式
BrowserRouter(推荐)
使用 HTML5 History API:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserDetail />} />
</Routes>
</BrowserRouter>
);
}
// URL 示例: https://example.com/about
// 需要服务器配置支持(所有路径返回 index.html)
HashRouter
使用 URL hash:
import { HashRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</HashRouter>
);
}
// URL 示例: https://example.com/#/about
// 无需服务器配置,但 SEO 不友好
两种模式对比
| 特性 | BrowserRouter | HashRouter |
|---|---|---|
| URL 格式 | /path | /#/path |
| 实现原理 | History API | hashchange 事件 |
| 服务器配置 | 需要配置 | 无需配置 |
| SEO | 友好 | 不友好 |
| 兼容性 | IE10+ | IE8+ |
| 推荐场景 | 生产环境 | 静态托管、兼容需求 |
基本用法
路由配置
import {
createBrowserRouter,
RouterProvider,
Route,
createRoutesFromElements
} from 'react-router-dom';
// 方式1: 对象配置(推荐)
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: 'users', element: <Users />,
children: [
{ path: ':userId', element: <UserDetail /> }
]
},
{ path: '*', element: <NotFound /> }
]
}
]);
// 方式2: JSX 配置
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Layout />} errorElement={<ErrorPage />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users" element={<Users />}>
<Route path=":userId" element={<UserDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
)
);
function App() {
return <RouterProvider router={router} />;
}
嵌套路由与 Outlet
// Layout.tsx - 父路由组件
import { Outlet, Link } from 'react-router-dom';
function Layout() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/users">用户</Link>
</nav>
<main>
{/* 子路由渲染位置 */}
<Outlet />
</main>
<footer>Footer</footer>
</div>
);
}
// Users.tsx - 也可以有 Outlet
function Users() {
return (
<div>
<h1>用户列表</h1>
<UserList />
{/* 嵌套子路由 /users/:userId 渲染位置 */}
<Outlet />
</div>
);
}
Link 和 NavLink
import { Link, NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* 基础链接 */}
<Link to="/about">关于</Link>
{/* 带状态的链接 */}
<Link to="/users" state={{ from: 'nav' }}>
用户
</Link>
{/* NavLink: 自动添加 active 类 */}
<NavLink
to="/dashboard"
className={({ isActive, isPending }) =>
isActive ? 'active' : isPending ? 'pending' : ''
}
>
控制台
</NavLink>
{/* NavLink: 自定义样式 */}
<NavLink
to="/settings"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'red' : 'black'
})}
>
设置
</NavLink>
</nav>
);
}
Hooks API
useParams
获取动态路由参数:
import { useParams } from 'react-router-dom';
// 路由: /users/:userId/posts/:postId
function PostDetail() {
const { userId, postId } = useParams<{
userId: string;
postId: string;
}>();
return (
<div>
<p>用户 ID: {userId}</p>
<p>文章 ID: {postId}</p>
</div>
);
}
useNavigate
编程式导航:
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const success = await login(credentials);
if (success) {
// 导航到首页
navigate('/');
// 替换当前历史记录
navigate('/dashboard', { replace: true });
// 携带状态
navigate('/profile', { state: { from: 'login' } });
// 后退
navigate(-1);
// 前进
navigate(1);
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
useLocation
获取当前位置信息:
import { useLocation } from 'react-router-dom';
function CurrentPage() {
const location = useLocation();
// location 对象结构
// {
// pathname: "/users/123",
// search: "?tab=posts",
// hash: "#section1",
// state: { from: "nav" },
// key: "default"
// }
return (
<div>
<p>路径: {location.pathname}</p>
<p>查询: {location.search}</p>
<p>哈希: {location.hash}</p>
<p>状态: {JSON.stringify(location.state)}</p>
</div>
);
}
useSearchParams
管理 URL 查询参数:
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
// 获取参数 /search?q=react&page=2
const query = searchParams.get('q'); // "react"
const page = searchParams.get('page'); // "2"
function handleSearch(term: string) {
// 设置参数
setSearchParams({ q: term, page: '1' });
}
function nextPage() {
// 更新参数
setSearchParams(prev => {
prev.set('page', String(Number(prev.get('page') || 1) + 1));
return prev;
});
}
return (
<div>
<input
value={query || ''}
onChange={e => handleSearch(e.target.value)}
/>
<button onClick={nextPage}>下一页</button>
</div>
);
}
useMatch
匹配路由模式:
import { useMatch } from 'react-router-dom';
function UserAvatar() {
// 检查是否匹配特定路由
const match = useMatch('/users/:userId');
if (match) {
const { userId } = match.params;
return <Avatar userId={userId} />;
}
return <DefaultAvatar />;
}
数据加载(v6.4+)
loader 函数
路由渲染前加载数据:
import {
createBrowserRouter,
useLoaderData,
defer,
Await
} from 'react-router-dom';
import { Suspense } from 'react';
// 定义 loader
async function userLoader({ params }: { params: { userId: string } }) {
const response = await fetch(`/api/users/${params.userId}`);
if (!response.ok) {
throw new Response('User not found', { status: 404 });
}
return response.json();
}
// 路由配置
const router = createBrowserRouter([
{
path: '/users/:userId',
element: <UserDetail />,
loader: userLoader,
errorElement: <UserError />
}
]);
// 组件中使用数据
function UserDetail() {
const user = useLoaderData() as User;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// 延迟加载(流式渲染)
async function dashboardLoader() {
return defer({
user: getCurrentUser(), // 立即加载
posts: fetchPosts(), // 延迟加载
comments: fetchComments() // 延迟加载
});
}
function Dashboard() {
const { user, posts, comments } = useLoaderData() as {
user: User;
posts: Promise<Post[]>;
comments: Promise<Comment[]>;
};
return (
<div>
<h1>欢迎, {user.name}</h1>
<Suspense fallback={<p>加载文章...</p>}>
<Await resolve={posts}>
{(resolvedPosts) => <PostList posts={resolvedPosts} />}
</Await>
</Suspense>
</div>
);
}
action 函数
处理表单提交:
import {
Form,
useActionData,
useNavigation,
redirect
} from 'react-router-dom';
// 定义 action
async function createUserAction({ request }: { request: Request }) {
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
// 验证
const errors: Record<string, string> = {};
if (!name) errors.name = '姓名必填';
if (!email) errors.email = '邮箱必填';
if (Object.keys(errors).length) {
return { errors };
}
// 创建用户
await createUser({ name, email });
// 重定向
return redirect('/users');
}
// 路由配置
const router = createBrowserRouter([
{
path: '/users/new',
element: <CreateUser />,
action: createUserAction
}
]);
// 组件
function CreateUser() {
const actionData = useActionData() as { errors?: Record<string, string> };
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
return (
<Form method="post">
<div>
<label>姓名</label>
<input name="name" />
{actionData?.errors?.name && (
<span className="error">{actionData.errors.name}</span>
)}
</div>
<div>
<label>邮箱</label>
<input name="email" type="email" />
{actionData?.errors?.email && (
<span className="error">{actionData.errors.email}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '创建用户'}
</button>
</Form>
);
}
路由守卫
认证守卫
import { Navigate, useLocation, Outlet } from 'react-router-dom';
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
}
function ProtectedRoute() {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
// 重定向到登录页,保存当前位置
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
// 路由配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'login', element: <Login /> },
// 受保护的路由
{
element: <ProtectedRoute />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'profile', element: <Profile /> },
{ path: 'settings', element: <Settings /> }
]
}
]
}
]);
// 登录后重定向回原页面
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = (location.state as { from?: Location })?.from?.pathname || '/';
async function handleLogin() {
await login();
navigate(from, { replace: true });
}
return <button onClick={handleLogin}>登录</button>;
}
权限守卫
interface PermissionGuardProps {
permissions: string[];
children: React.ReactNode;
fallback?: React.ReactNode;
}
function PermissionGuard({
permissions,
children,
fallback = <Forbidden />
}: PermissionGuardProps) {
const { user } = useAuth();
const hasPermission = permissions.every(
p => user?.permissions.includes(p)
);
if (!hasPermission) {
return fallback;
}
return <>{children}</>;
}
// 使用
function AdminPage() {
return (
<PermissionGuard permissions={['admin:read', 'admin:write']}>
<AdminDashboard />
</PermissionGuard>
);
}
懒加载路由
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
// 懒加载组件
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const UserList = lazy(() => import('./pages/UserList'));
// 加载组件
function PageLoader() {
return <div className="page-loader">加载中...</div>;
}
// 包装懒加载组件
function lazyLoad(Component: React.LazyExoticComponent<() => JSX.Element>) {
return (
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
);
}
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: 'dashboard', element: lazyLoad(Dashboard) },
{ path: 'settings', element: lazyLoad(Settings) },
{ path: 'users', element: lazyLoad(UserList) }
]
}
]);
// 或者使用 route.lazy(v6.4+)
const router = createBrowserRouter([
{
path: '/dashboard',
lazy: async () => {
const { Dashboard } = await import('./pages/Dashboard');
return { Component: Dashboard };
}
}
]);
常见面试问题
Q1: React Router 的实现原理是什么?
答案:
React Router 基于监听 URL 变化和条件渲染实现:
// 简化版实现原理
import { createContext, useContext, useState, useEffect } from 'react';
// 1. 创建 Router Context
const RouterContext = createContext<{
pathname: string;
navigate: (to: string) => void;
} | null>(null);
// 2. BrowserRouter 监听 popstate
function BrowserRouter({ children }: { children: React.ReactNode }) {
const [pathname, setPathname] = useState(window.location.pathname);
useEffect(() => {
const handlePopState = () => {
setPathname(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
const navigate = (to: string) => {
window.history.pushState({}, '', to);
setPathname(to);
};
return (
<RouterContext.Provider value={{ pathname, navigate }}>
{children}
</RouterContext.Provider>
);
}
// 3. Route 条件渲染
function Route({ path, element }: { path: string; element: React.ReactNode }) {
const { pathname } = useContext(RouterContext)!;
// 简单匹配(实际实现更复杂)
if (pathname === path) {
return <>{element}</>;
}
return null;
}
// 4. Link 改变 URL
function Link({ to, children }: { to: string; children: React.ReactNode }) {
const { navigate } = useContext(RouterContext)!;
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
navigate(to);
};
return <a href={to} onClick={handleClick}>{children}</a>;
}
Q2: BrowserRouter 和 HashRouter 的区别?
答案:
| 特性 | BrowserRouter | HashRouter |
|---|---|---|
| URL | /users/123 | /#/users/123 |
| API | history.pushState | location.hash |
| 服务器 | 需配置 fallback | 无需配置 |
| SEO | 友好 | 不友好 |
| 原理 | 监听 popstate | 监听 hashchange |
// BrowserRouter 需要服务器配置
// Nginx 示例
// location / {
// try_files $uri $uri/ /index.html;
// }
// HashRouter 无需配置,因为 # 后的内容不会发送到服务器
Q3: 如何实现路由懒加载?
答案:
import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';
// 方式1: React.lazy + Suspense
const Dashboard = lazy(() => import('./Dashboard'));
const router = createBrowserRouter([
{
path: '/dashboard',
element: (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
)
}
]);
// 方式2: route.lazy(推荐,v6.4+)
const router = createBrowserRouter([
{
path: '/dashboard',
lazy: () => import('./Dashboard').then(m => ({ Component: m.default }))
}
]);
// 方式3: 预加载
const DashboardPromise = import('./Dashboard');
const Dashboard = lazy(() => DashboardPromise);
// 鼠标悬停时预加载
<Link to="/dashboard" onMouseEnter={() => import('./Dashboard')}>
控制台
</Link>
Q4: 如何实现路由守卫/权限控制?
答案:
// 1. 认证守卫组件
function RequireAuth({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// 2. 在路由配置中使用
const router = createBrowserRouter([
{
element: <RequireAuth><Outlet /></RequireAuth>,
children: [
{ path: '/dashboard', element: <Dashboard /> },
{ path: '/profile', element: <Profile /> }
]
}
]);
// 3. 结合 loader 进行权限校验
async function adminLoader() {
const user = await getUser();
if (!user || !user.isAdmin) {
throw redirect('/unauthorized');
}
return user;
}
const router = createBrowserRouter([
{
path: '/admin',
element: <AdminPanel />,
loader: adminLoader
}
]);
Q5: loader 和 useEffect 获取数据有什么区别?
答案:
| 特性 | loader | useEffect |
|---|---|---|
| 时机 | 路由切换前 | 组件挂载后 |
| 阻塞 | 阻塞渲染 | 不阻塞 |
| 骨架屏 | 需要配合 defer | 天然支持 |
| 错误处理 | errorElement | try/catch |
| 数据位置 | 路由级别 | 组件级别 |
| 并行请求 | 自动并行 | 需手动 Promise.all |
// loader: 数据准备好再渲染
{
path: '/users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
return fetch(`/api/users/${params.id}`).then(r => r.json());
}
}
// useEffect: 先渲染再请求
function UserDetail() {
const [user, setUser] = useState(null);
const { id } = useParams();
useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser);
}, [id]);
if (!user) return <Loading />;
return <div>{user.name}</div>;
}