跳到主要内容

React 路由

在现代单页面应用(SPA)中,路由是实现页面导航的核心机制。React Router 是 React 生态中最流行的路由库,它允许你在应用中实现声明式的路由导航,同时保持 UI 与 URL 的同步。

路由的基本概念

什么是客户端路由?

传统的多页面应用中,每次导航都会向服务器请求新页面。而单页面应用使用客户端路由,只加载一次页面,然后通过 JavaScript 动态更新内容,实现页面切换而不刷新浏览器。

传统多页面应用:
用户点击 → 请求服务器 → 返回新 HTML 页面 → 浏览器刷新

单页面应用:
用户点击 → JavaScript 处理 → 更新 UI + URL → 无刷新切换

React Router 的核心优势

  • 声明式路由:像声明组件一样声明路由
  • 嵌套路由:支持复杂的布局嵌套
  • 代码分割:支持路由级别的懒加载
  • 丰富的 API:提供多种 Hooks 和组件

安装与配置

安装

npm install react-router-dom
# 或
yarn add react-router-dom
# 或
pnpm add react-router-dom

基本配置

React Router 提供两种路由器组件:

  • BrowserRouter:使用 HTML5 History API(推荐用于生产)
  • MemoryRouter:将 URL 存储在内存中(适合测试和演示)
实时编辑器
function BasicRouterDemo() {
  // 使用 MemoryRouter 进行演示
  const Home = () => (
    <div style={{ padding: '20px', background: '#f0f9ff', borderRadius: '8px' }}>
      <h2>首页</h2>
      <p>欢迎来到 React Router 教程!</p>
    </div>
  );

  const About = () => (
    <div style={{ padding: '20px', background: '#fef3c7', borderRadius: '8px' }}>
      <h2>关于我们</h2>
      <p>这是一个关于 React Router 的教程示例。</p>
    </div>
  );

  const Contact = () => (
    <div style={{ padding: '20px', background: '#dcfce7', borderRadius: '8px' }}>
      <h2>联系我们</h2>
      <p>邮箱: [email protected]</p>
    </div>
  );

  return (
    <MemoryRouter initialEntries={['/']}>
      <nav style={{ 
        display: 'flex', 
        gap: '10px', 
        marginBottom: '15px',
        padding: '10px',
        background: '#f8fafc',
        borderRadius: '8px'
      }}>
        <Link 
          to="/" 
          style={({ isActive }) => ({
            padding: '8px 16px',
            background: isActive ? '#3b82f6' : '#e2e8f0',
            color: isActive ? 'white' : '#64748b',
            textDecoration: 'none',
            borderRadius: '4px'
          })}
        >
          首页
        </Link>
        <Link to="/about" style={{ padding: '8px 16px', background: '#e2e8f0', color: '#64748b', textDecoration: 'none', borderRadius: '4px' }}>
          关于
        </Link>
        <Link to="/contact" style={{ padding: '8px 16px', background: '#e2e8f0', color: '#64748b', textDecoration: 'none', borderRadius: '4px' }}>
          联系
        </Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

核心组件详解

BrowserRouter

BrowserRouter 是最常用的路由器,它使用 HTML5 History API 来保持 UI 与 URL 同步。

// 在应用入口使用
import { BrowserRouter } from 'react-router-dom';

function App() {
return (
<BrowserRouter>
<Routes>
{/* 路由配置 */}
</Routes>
</BrowserRouter>
);
}

// 或者在 index.jsx 中包裹
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);

Routes 和 Route

Routes 是路由的容器,Route 定义单个路由规则。

<Routes>
{/* 精确匹配 */}
<Route path="/" element={<Home />} />

{/* 带参数的路由 */}
<Route path="/users/:id" element={<UserProfile />} />

{/* 嵌套路由 */}
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>

{/* 404 路由 */}
<Route path="*" element={<NotFound />} />
</Routes>

Link 用于声明式导航,NavLink 是特殊的 Link,可以添加激活状态样式。

实时编辑器
function NavLinkDemo() {
  return (
    <MemoryRouter initialEntries={['/']}>
      <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
        <NavLink
          to="/"
          style={({ isActive }) => ({
            padding: '8px 16px',
            background: isActive ? '#3b82f6' : '#e2e8f0',
            color: isActive ? 'white' : '#64748b',
            textDecoration: 'none',
            borderRadius: '4px',
            fontWeight: isActive ? 'bold' : 'normal'
          })}
        >
          首页
        </NavLink>
        <NavLink
          to="/products"
          style={({ isActive }) => ({
            padding: '8px 16px',
            background: isActive ? '#3b82f6' : '#e2e8f0',
            color: isActive ? 'white' : '#64748b',
            textDecoration: 'none',
            borderRadius: '4px',
            fontWeight: isActive ? 'bold' : 'normal'
          })}
        >
          产品
        </NavLink>
        <NavLink
          to="/about"
          style={({ isActive }) => ({
            padding: '8px 16px',
            background: isActive ? '#3b82f6' : '#e2e8f0',
            color: isActive ? 'white' : '#64748b',
            textDecoration: 'none',
            borderRadius: '4px',
            fontWeight: isActive ? 'bold' : 'normal'
          })}
        >
          关于
        </NavLink>
      </nav>
      
      <Routes>
        <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff' }}>首页内容</div>} />
        <Route path="/products" element={<div style={{ padding: '20px', background: '#fef3c7' }}>产品列表</div>} />
        <Route path="/about" element={<div style={{ padding: '20px', background: '#dcfce7' }}>关于我们</div>} />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

NavLink 还可以使用 className 属性:

<NavLink
to="/about"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
关于
</NavLink>

路由 Hooks

useNavigate:编程式导航

useNavigate 返回一个函数,用于在代码中执行导航。

实时编辑器
function NavigateDemo() {
  const navigate = useNavigate();
  const [path, setPath] = useState('');

  return (
    <MemoryRouter initialEntries={['/']}>
      <div style={{ marginBottom: '10px' }}>
        <input
          value={path}
          onChange={(e) => setPath(e.target.value)}
          placeholder="输入路径..."
          style={{ padding: '8px', marginRight: '10px' }}
        />
        <button onClick={() => navigate(path || '/')} style={{ padding: '8px 16px' }}>
          跳转
        </button>
      </div>

      <div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
        <button onClick={() => navigate(-1)} style={{ padding: '8px 16px' }}>
          ← 后退
        </button>
        <button onClick={() => navigate(1)} style={{ padding: '8px 16px' }}>
          前进 →
        </button>
        <button onClick={() => navigate('/', { replace: true })} style={{ padding: '8px 16px' }}>
          返回首页
        </button>
      </div>

      <Routes>
        <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff' }}>首页 - 尝试输入路径跳转</div>} />
        <Route path="/page1" element={<div style={{ padding: '20px', background: '#fef3c7' }}>页面 1</div>} />
        <Route path="/page2" element={<div style={{ padding: '20px', background: '#dcfce7' }}>页面 2</div>} />
        <Route path="*" element={<div style={{ padding: '20px', background: '#fee2e2' }}>404 - 页面不存在</div>} />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

navigate 函数的用法:

const navigate = useNavigate();

// 导航到指定路径
navigate('/dashboard');

// 相对路径
navigate('../settings');

// 后退和前进
navigate(-1); // 后退一步
navigate(1); // 前进一步
navigate(-2); // 后退两步

// 替换当前历史记录
navigate('/login', { replace: true });

// 携带 state
navigate('/dashboard', { state: { from: 'login' } });

useParams:获取路由参数

useParams 用于获取 URL 中的动态参数。

实时编辑器
function ParamsDemo() {
  const UserPage = () => {
    const { id } = useParams();
    return (
      <div style={{ padding: '20px', background: '#f0f9ff', borderRadius: '8px' }}>
        <h3>用户详情</h3>
        <p>用户 ID: <strong style={{ color: '#3b82f6' }}>{id}</strong></p>
      </div>
    );
  };

  const ProductPage = () => {
    const { category, productId } = useParams();
    return (
      <div style={{ padding: '20px', background: '#fef3c7', borderRadius: '8px' }}>
        <h3>产品详情</h3>
        <p>分类: <strong>{category}</strong></p>
        <p>产品 ID: <strong style={{ color: '#d97706' }}>{productId}</strong></p>
      </div>
    );
  };

  return (
    <MemoryRouter initialEntries={['/user/123']}>
      <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px', flexWrap: 'wrap' }}>
        <Link to="/user/123" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>用户 123</Link>
        <Link to="/user/456" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>用户 456</Link>
        <Link to="/products/electronics/laptop" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>电子产品/笔记本</Link>
        <Link to="/products/clothing/shirt" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>服装/衬衫</Link>
      </nav>

      <Routes>
        <Route path="/user/:id" element={<UserPage />} />
        <Route path="/products/:category/:productId" element={<ProductPage />} />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

useLocation:获取当前位置

useLocation 返回当前 URL 的位置信息对象。

实时编辑器
function LocationDemo() {
  const location = useLocation();

  return (
    <MemoryRouter initialEntries={['/']}>
      <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
        <Link to="/" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>首页</Link>
        <Link to="/about?ref=nav" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>关于 (带查询参数)</Link>
        <Link to="/contact#section1" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>联系 (带锚点)</Link>
      </nav>

      <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px', marginBottom: '15px' }}>
        <h4 style={{ margin: '0 0 10px' }}>Location 对象:</h4>
        <pre style={{ fontSize: '12px', margin: 0, overflow: 'auto' }}>
          {JSON.stringify({
            pathname: location.pathname,
            search: location.search,
            hash: location.hash,
            state: location.state,
            key: location.key
          }, null, 2)}
        </pre>
      </div>

      <Routes>
        <Route path="/" element={<div style={{ padding: '20px' }}>首页</div>} />
        <Route path="/about" element={<div style={{ padding: '20px' }}>关于页面</div>} />
        <Route path="/contact" element={<div style={{ padding: '20px' }}>联系页面</div>} />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

location 对象包含:

  • pathname:URL 路径
  • search:查询字符串(如 ?ref=nav
  • hash:锚点(如 #section1
  • state:导航时传递的状态
  • key:唯一标识符

useSearchParams:处理查询参数

useSearchParams 用于读取和修改 URL 查询参数,类似 useState

实时编辑器
function SearchParamsDemo() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  const page = searchParams.get('page') || '1';
  const sort = searchParams.get('sort') || 'date';
  const category = searchParams.get('category') || 'all';

  const updateParams = (key, value) => {
    setSearchParams(prev => {
      prev.set(key, value);
      return prev;
    });
  };

  return (
    <MemoryRouter initialEntries={['/?page=1&sort=date']}>
      <div style={{ marginBottom: '15px' }}>
        <label style={{ marginRight: '10px' }}>
          页码:
          <select 
            value={page} 
            onChange={(e) => updateParams('page', e.target.value)}
            style={{ marginLeft: '5px', padding: '5px' }}
          >
            {[1, 2, 3, 4, 5].map(p => (
              <option key={p} value={p}>{p}</option>
            ))}
          </select>
        </label>
        
        <label style={{ marginRight: '10px' }}>
          排序:
          <select 
            value={sort} 
            onChange={(e) => updateParams('sort', e.target.value)}
            style={{ marginLeft: '5px', padding: '5px' }}
          >
            <option value="date">按日期</option>
            <option value="price">按价格</option>
            <option value="name">按名称</option>
          </select>
        </label>

        <label>
          分类:
          <select 
            value={category} 
            onChange={(e) => updateParams('category', e.target.value)}
            style={{ marginLeft: '5px', padding: '5px' }}
          >
            <option value="all">全部</option>
            <option value="electronics">电子产品</option>
            <option value="clothing">服装</option>
          </select>
        </label>
      </div>

      <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px' }}>
        <p><strong>当前查询参数:</strong></p>
        <p>页码: {page}</p>
        <p>排序: {sort}</p>
        <p>分类: {category}</p>
        <p style={{ fontSize: '12px', color: '#64748b' }}>
          URL: ?{searchParams.toString()}
        </p>
      </div>
    </MemoryRouter>
  );
}
结果
Loading...

嵌套路由

嵌套路由允许你在父路由中渲染子路由,实现复杂的布局结构。

使用 Outlet

Outlet 是子路由的渲染占位符。

实时编辑器
function NestedRoutesDemo() {
  // 布局组件
  const Layout = () => (
    <div style={{ border: '2px solid #3b82f6', borderRadius: '8px', overflow: 'hidden' }}>
      <header style={{ padding: '15px', background: '#3b82f6', color: 'white' }}>
        <h2 style={{ margin: 0 }}>仪表盘</h2>
      </header>
      <div style={{ display: 'flex' }}>
        <nav style={{ 
          width: '150px', 
          padding: '10px', 
          background: '#f1f5f9',
          minHeight: '200px'
        }}>
          <NavLink 
            to="" 
            end
            style={({ isActive }) => ({
              display: 'block',
              padding: '8px',
              background: isActive ? '#dbeafe' : 'transparent',
              color: isActive ? '#1d4ed8' : '#64748b',
              textDecoration: 'none',
              borderRadius: '4px',
              marginBottom: '4px'
            })}
          >
            概览
          </NavLink>
          <NavLink 
            to="stats"
            style={({ isActive }) => ({
              display: 'block',
              padding: '8px',
              background: isActive ? '#dbeafe' : 'transparent',
              color: isActive ? '#1d4ed8' : '#64748b',
              textDecoration: 'none',
              borderRadius: '4px',
              marginBottom: '4px'
            })}
          >
            统计
          </NavLink>
          <NavLink 
            to="settings"
            style={({ isActive }) => ({
              display: 'block',
              padding: '8px',
              background: isActive ? '#dbeafe' : 'transparent',
              color: isActive ? '#1d4ed8' : '#64748b',
              textDecoration: 'none',
              borderRadius: '4px'
            })}
          >
            设置
          </NavLink>
        </nav>
        <main style={{ flex: 1, padding: '15px' }}>
          <Outlet />
        </main>
      </div>
    </div>
  );

  const Overview = () => <div>
    <h3>概览</h3>
    <p>欢迎回来!这是仪表盘概览页面。</p>
  </div>;

  const Stats = () => <div>
    <h3>统计</h3>
    <p>用户数: 1,234</p>
    <p>访问量: 56,789</p>
  </div>;

  const Settings = () => <div>
    <h3>设置</h3>
    <p>在这里配置您的偏好设置。</p>
  </div>;

  return (
    <MemoryRouter initialEntries={['/dashboard']}>
      <Routes>
        <Route path="/dashboard" element={<Layout />}>
          <Route index element={<Overview />} />
          <Route path="stats" element={<Stats />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

index 路由

index 路由是父路径的默认子路由:

<Route path="/dashboard" element={<Layout />}>
{/* 访问 /dashboard 时显示 */}
<Route index element={<DashboardHome />} />

{/* 访问 /dashboard/settings 时显示 */}
<Route path="settings" element={<Settings />} />
</Route>

路由守卫(保护路由)

在实际应用中,某些页面需要用户登录后才能访问。我们可以创建一个保护路由组件。

实时编辑器
function ProtectedRouteDemo() {
  // 模拟登录状态
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // 保护路由组件
  const ProtectedRoute = ({ children }) => {
    const location = useLocation();
    
    if (!isLoggedIn) {
      // 重定向到登录页,并保存原始路径
      return <Navigate to="/login" state={{ from: location }} replace />;
    }
    
    return children;
  };

  const LoginPage = () => {
    const navigate = useNavigate();
    const location = useLocation();
    const from = location.state?.from?.pathname || '/';

    const handleLogin = () => {
      setIsLoggedIn(true);
      navigate(from, { replace: true });
    };

    return (
      <div style={{ padding: '20px', background: '#fee2e2', borderRadius: '8px' }}>
        <h3>请登录</h3>
        <p style={{ fontSize: '12px', color: '#64748b' }}>
          {location.state?.from ? `登录后将跳转到: ${location.state.from.pathname}` : ''}
        </p>
        <button 
          onClick={handleLogin}
          style={{ padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }}
        >
          模拟登录
        </button>
      </div>
    );
  };

  const DashboardPage = () => (
    <div style={{ padding: '20px', background: '#dcfce7', borderRadius: '8px' }}>
      <h3>仪表盘 (受保护)</h3>
      <p>欢迎回来!这是受保护的页面。</p>
      <button 
        onClick={() => setIsLoggedIn(false)}
        style={{ padding: '8px 16px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }}
      >
        退出登录
      </button>
    </div>
  );

  return (
    <MemoryRouter initialEntries={['/']}>
      <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px', alignItems: 'center' }}>
        <Link to="/" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>首页</Link>
        <Link to="/dashboard" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>仪表盘</Link>
        <span style={{ marginLeft: 'auto', fontSize: '12px', color: isLoggedIn ? '#22c55e' : '#ef4444' }}>
          {isLoggedIn ? '✓ 已登录' : '✗ 未登录'}
        </span>
      </nav>

      <Routes>
        <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff', borderRadius: '8px' }}>首页 - 公开页面</div>} />
        <Route path="/login" element={<LoginPage />} />
        <Route 
          path="/dashboard" 
          element={
            <ProtectedRoute>
              <DashboardPage />
            </ProtectedRoute>
          } 
        />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

懒加载路由

使用 React.lazySuspense 可以实现路由级别的代码分割,减少首屏加载时间。

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

404 页面处理

使用通配符路由捕获所有未匹配的路径。

实时编辑器
function NotFoundDemo() {
  return (
    <MemoryRouter initialEntries={['/']}>
      <nav style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
        <Link to="/" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>首页</Link>
        <Link to="/nonexistent" style={{ padding: '6px 12px', background: '#e2e8f0', borderRadius: '4px', textDecoration: 'none' }}>不存在的页面</Link>
      </nav>

      <Routes>
        <Route path="/" element={<div style={{ padding: '20px', background: '#f0f9ff' }}>首页</div>} />
        <Route 
          path="*" 
          element={
            <div style={{ 
              padding: '40px', 
              background: '#fee2e2', 
              borderRadius: '8px', 
              textAlign: 'center' 
            }}>
              <h1 style={{ margin: '0 0 10px', fontSize: '48px' }}>404</h1>
              <p style={{ margin: '0 0 15px', color: '#64748b' }}>页面不存在</p>
              <Link 
                to="/" 
                style={{ 
                  padding: '10px 20px', 
                  background: '#3b82f6', 
                  color: 'white', 
                  textDecoration: 'none',
                  borderRadius: '4px'
                }}
              >
                返回首页
              </Link>
            </div>
          } 
        />
      </Routes>
    </MemoryRouter>
  );
}
结果
Loading...

路由配置最佳实践

集中式路由配置

对于大型应用,可以将路由配置集中管理:

// routes.js
const routes = [
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{
path: 'dashboard',
element: <ProtectedRoute><Dashboard /></ProtectedRoute>,
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'settings', element: <Settings /> }
]
},
{ path: '*', element: <NotFound /> }
]
}
];

// 使用递归渲染路由
function renderRoutes(routes) {
return routes.map((route, i) => (
<Route
key={i}
path={route.path}
element={route.element}
>
{route.children && renderRoutes(route.children)}
</Route>
));
}

滚动恢复

在导航时恢复滚动位置:

function ScrollToTop() {
const { pathname } = useLocation();

useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);

return null;
}

// 在应用中使用
<BrowserRouter>
<ScrollToTop />
<Routes>
{/* ... */}
</Routes>
</BrowserRouter>

小结

  1. BrowserRouter:使用 HTML5 History API 的路由器
  2. Routes/Route:声明式定义路由规则
  3. Link/NavLink:声明式导航,NavLink 支持激活状态
  4. useNavigate:编程式导航
  5. useParams:获取 URL 参数
  6. useLocation:获取当前位置信息
  7. useSearchParams:处理查询参数
  8. Outlet:嵌套路由的占位符
  9. 保护路由:实现权限控制
  10. 懒加载:代码分割优化性能

练习

  1. 实现一个带有登录保护的完整路由系统
  2. 创建一个多级嵌套的路由结构(如 /admin/users/:id/edit)
  3. 使用 useSearchParams 实现一个带筛选功能的列表页
  4. 实现路由级别的懒加载
  5. 创建一个面包屑导航组件,根据当前路由自动生成

参考资源