React Router DOM 教程

项目概述

React Router DOM 是 React Router 的 DOM 绑定,专门为 Web 浏览器环境设计。它提供了声明式的路由解决方案,支持客户端路由、嵌套路由、参数路由、导航守卫等功能,与 React 生态系统深度集成,使构建单页应用(SPA)变得更加简单和直观。

安装设置

1. 安装 React Router DOM

# 使用 npm
npm install react-router-dom

# 使用 yarn
yarn add react-router-dom

# 使用 pnpm
pnpm add react-router-dom

2. 基本配置

在应用的根组件中设置路由配置:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

3. 路由配置

// App.jsx
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';

function App() {
  return (
    <div>
      {/* 导航链接 */}
      <nav>
        <Link to="/">首页</Link> |
        <Link to="/about">关于我们</Link> |
        <Link to="/contact">联系我们</Link>
      </nav>

      {/* 路由配置 */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </div>
  );
}

export default App;

核心功能

1. 路由组件

BrowserRouter

使用 HTML5 History API 实现的路由器,提供干净的 URL(无哈希)。

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

function Root() {
  return (
    <BrowserRouter>
      <App />
    </BrowserRouter>
  );
}

HashRouter

使用 URL 哈希实现的路由器,适合无法配置服务器的环境。

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

function Root() {
  return (
    <HashRouter>
      <App />
    </HashRouter>
  );
}

MemoryRouter

使用内存存储历史记录的路由器,适合测试和非浏览器环境。

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

function TestWrapper() {
  return (
    <MemoryRouter initialEntries={['/']}>
      <App />
    </MemoryRouter>
  );
}

2. 导航组件

创建导航链接,不会触发页面刷新。

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

function Navigation() {
  return (
    <nav>
      {/* 基本链接 */}
      <Link to="/">首页</Link>
      
      {/* 带状态的链接 */}
      <Link to="/dashboard" state={{ from: 'home' }}>仪表盘</Link>
      
      {/* 带查询参数 */}
      <Link to="/search?q=react">搜索 React</Link>
    </nav>
  );
}

带活跃状态的链接,可根据路由状态应用不同样式。

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

function Navigation() {
  return (
    <nav>
      {/* 带类名的活跃链接 */}
      <NavLink 
        to="/" 
        className={({ isActive }) => isActive ? 'active-link' : 'link'}
      >
        首页
      </NavLink>
      
      {/* 带样式的活跃链接 */}
      <NavLink 
        to="/about"
        style={({ isActive }) => ({
          color: isActive ? 'red' : 'blue',
          fontWeight: isActive ? 'bold' : 'normal'
        })}
      >
        关于我们
      </NavLink>
      
      {/* 精确匹配 */}
      <NavLink to="/" end>首页</NavLink>
    </nav>
  );
}

3. 路由渲染

Routes 和 Route

Routes 组件用于包裹路由配置,Route 组件定义单个路由规则。

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

function App() {
  return (
    <Routes>
      {/* 基本路由 */}
      <Route path="/" element={<Home />} />
      
      {/* 参数路由 */}
      <Route path="/users/:id" element={<UserDetail />} />
      
      {/* 嵌套路由 */}
      <Route path="/dashboard" element={<Dashboard />}>
        <Route index element={<DashboardHome />} />
        <Route path="profile" element={<Profile />} />
        <Route path="settings" element={<Settings />} />
      </Route>
      
      {/* 404 页面 */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Outlet

用于渲染嵌套路由的组件。

import { Outlet, Link } from 'react-router-dom';

function Dashboard() {
  return (
    <div>
      <h1>仪表盘</h1>
      <nav>
        <Link to="">首页</Link> |
        <Link to="profile">个人资料</Link> |
        <Link to="settings">设置</Link>
      </nav>
      {/* 嵌套路由将在这里渲染 */}
      <Outlet />
    </div>
  );
}

4. Hooks API

useNavigate

用于编程式导航。

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

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

  const handleLogin = () => {
    // 登录成功后导航
    navigate('/dashboard');
    
    // 带替换(不会在历史记录中添加新条目)
    navigate('/dashboard', { replace: true });
    
    // 带状态
    navigate('/dashboard', { 
      state: { token: 'abc123' },
      replace: true 
    });
    
    // 后退一步
    navigate(-1);
    
    // 前进一步
    navigate(1);
  };

  return (
    <div>
      <h1>登录</h1>
      <button onClick={handleLogin}>登录</button>
    </div>
  );
}

useRoute

用于获取当前路由信息。

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

function UserDetail() {
  const route = useRoute();
  const userId = route.params.id;
  const query = route.query;

  return (
    <div>
      <h1>用户详情 - ID: {userId}</h1>
      {query.ref && <p>来源: {query.ref}</p>}
    </div>
  );
}

useLocation

用于获取当前位置信息。

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

function Page() {
  const location = useLocation();
  
  // 获取路径
  const path = location.pathname;
  
  // 获取查询参数
  const search = location.search;
  
  // 获取哈希
  const hash = location.hash;
  
  // 获取状态
  const state = location.state;

  return (
    <div>
      <h1>当前路径: {path}</h1>
      {state && <p>状态: {JSON.stringify(state)}</p>}
    </div>
  );
}

useMatch

用于匹配指定路径。

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

function Navigation() {
  const homeMatch = useMatch('/');
  const aboutMatch = useMatch('/about');

  return (
    <nav>
      <a 
        href="/" 
        className={homeMatch ? 'active' : ''}
      >
        首页
      </a>
      <a 
        href="/about" 
        className={aboutMatch ? 'active' : ''}
      >
        关于我们
      </a>
    </nav>
  );
}

5. 导航守卫

使用高阶组件实现

import { Navigate, useLocation } from 'react-router-dom';

function RequireAuth({ children }) {
  const location = useLocation();
  const isLoggedIn = localStorage.getItem('token');

  if (!isLoggedIn) {
    // 未登录,重定向到登录页
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<LoginPage />} />
      {/* 受保护的路由 */}
      <Route 
        path="/dashboard" 
        element={
          <RequireAuth>
            <Dashboard />
          </RequireAuth>
        } 
      />
    </Routes>
  );
}

高级功能

1. 懒加载路由

使用 React.lazy() 和 Suspense 实现路由组件的懒加载,减小初始包大小。

import React, { lazy, Suspense } from 'react';
import { 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 Loading() {
  return <div>加载中...</div>;
}

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </Suspense>
  );
}

2. 嵌套布局

使用嵌套路由和布局组件实现复杂的页面结构。

import { Routes, Route, Outlet } from 'react-router-dom';

// 根布局
function RootLayout() {
  return (
    <div className="app">
      <header>
        <h1>我的应用</h1>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>
        <p>© 2024 我的应用</p>
      </footer>
    </div>
  );
}

// 认证布局
function AuthLayout() {
  return (
    <div className="auth-layout">
      <Outlet />
    </div>
  );
}

// 仪表盘布局
function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <nav>
          <Link to="">首页</Link>
          <Link to="profile">个人资料</Link>
          <Link to="settings">设置</Link>
        </nav>
      </aside>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

function App() {
  return (
    <Routes>
      {/* 根布局 */}
      <Route element={<RootLayout />}>
        {/* 公共路由 */}
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        
        {/* 认证路由 */}
        <Route element={<AuthLayout />}>
          <Route path="login" element={<LoginPage />} />
          <Route path="register" element={<RegisterPage />} />
        </Route>
        
        {/* 受保护的仪表盘路由 */}
        <Route 
          path="dashboard" 
          element={
            <RequireAuth>
              <DashboardLayout />
            </RequireAuth>
          }
        >
          <Route index element={<DashboardHome />} />
          <Route path="profile" element={<Profile />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Route>
    </Routes>
  );
}

3. 路由状态管理

使用 location.state 在路由之间传递数据,无需依赖全局状态管理。

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

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

  const handleProductClick = (product) => {
    // 传递产品数据到详情页
    navigate(`/product/${product.id}`, {
      state: { product }
    });
  };

  return (
    <div>
      {products.map(product => (
        <div 
          key={product.id} 
          onClick={() => handleProductClick(product)}
        >
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  );
}

function ProductDetail() {
  const location = useLocation();
  const product = location.state?.product;

  if (!product) {
    // 如果没有产品数据,重定向回列表页
    return <Navigate to="/products" replace />;
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
    </div>
  );
}

4. 搜索参数管理

使用 useSearchParams 钩子管理 URL 搜索参数。

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

function SearchPage() {
  // 获取和设置搜索参数
  const [searchParams, setSearchParams] = useSearchParams();
  
  // 获取当前搜索词
  const query = searchParams.get('q') || '';
  // 获取当前页码
  const page = parseInt(searchParams.get('page')) || 1;

  const handleSearch = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const newQuery = formData.get('query');
    
    // 更新搜索参数
    setSearchParams({ q: newQuery, page: 1 });
  };

  const handlePageChange = (newPage) => {
    // 保持搜索词,只更新页码
    setSearchParams({ q: query, page: newPage });
  };

  return (
    <div>
      <form onSubmit={handleSearch}>
        <input 
          type="text" 
          name="query" 
          defaultValue={query} 
          placeholder="搜索..."
        />
        <button type="submit">搜索</button>
      </form>

      <div>
        <h2>搜索结果: {query}</h2>
        <p>第 {page} 页</p>
        {/* 分页按钮 */}
        <button onClick={() => handlePageChange(page - 1)} disabled={page === 1}>
          上一页
        </button>
        <button onClick={() => handlePageChange(page + 1)}>
          下一页
        </button>
      </div>
    </div>
  );
}

5. 程序化导航

使用 useNavigate 钩子实现复杂的导航逻辑。

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

function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  
  // 从 location.state 获取重定向路径
  const from = location.state?.from?.pathname || '/';

  const handleLogin = async (credentials) => {
    try {
      // 登录逻辑
      const token = await login(credentials);
      localStorage.setItem('token', token);
      
      // 登录成功后重定向到之前的页面
      navigate(from, { replace: true });
    } catch (error) {
      console.error('登录失败:', error);
    }
  };

  return (
    <div>
      <h1>登录</h1>
      <LoginForm onSubmit={handleLogin} />
    </div>
  );
}

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

  const handleLogout = () => {
    // 清除登录状态
    localStorage.removeItem('token');
    // 重定向到登录页
    navigate('/login', { replace: true });
  };

  return <button onClick={handleLogout}>退出登录</button>;
}

实际应用场景

1. 认证系统

import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';

// 认证上下文
import { useAuth } from './AuthContext';

// 登录页面
function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();

  // 从 location.state 中获取重定向路径
  const from = location.state?.from?.pathname || '/';

  const handleSubmit = async (credentials) => {
    const success = await login(credentials);
    if (success) {
      // 登录成功后重定向到之前的页面
      navigate(from, { replace: true });
    }
  };

  return (
    <div>
      <h1>登录</h1>
      <LoginForm onSubmit={handleSubmit} />
    </div>
  );
}

// 注册页面
function RegisterPage() {
  const navigate = useNavigate();
  const { register } = useAuth();

  const handleSubmit = async (userData) => {
    const success = await register(userData);
    if (success) {
      navigate('/dashboard');
    }
  };

  return (
    <div>
      <h1>注册</h1>
      <RegisterForm onSubmit={handleSubmit} />
    </div>
  );
}

// 认证路由守卫
function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    // 未认证时重定向到登录页
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// 应用路由
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<LoginPage />} />
      <Route path="/register" element={<RegisterPage />} />
      <Route 
        path="/dashboard" 
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        } 
      />
      <Route 
        path="/profile" 
        element={
          <ProtectedRoute>
            <Profile /> 
          </ProtectedRoute>
        } 
      />
    </Routes>
  );
}

2. 电商应用

import { Routes, Route, Link, useParams, useNavigate, useLocation } from 'react-router-dom';

// 商品列表
function ProductList() {
  const navigate = useNavigate();

  const handleProductClick = (productId) => {
    navigate(`/product/${productId}`);
  };

  return (
    <div>
      <h1>商品列表</h1>
      <div className="product-grid">
        {products.map(product => (
          <div 
            key={product.id} 
            className="product-card"
            onClick={() => handleProductClick(product.id)}
          >
            <h3>{product.name}</h3>
            <p>${product.price}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

// 商品详情
function ProductDetail() {
  const { id } = useParams();
  const navigate = useNavigate();
  const location = useLocation();
  const product = location.state?.product || products.find(p => p.id === id);

  if (!product) {
    return <div>商品不存在</div>;
  }

  const handleAddToCart = () => {
    addToCart(product);
    navigate('/cart');
  };

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
      <button onClick={handleAddToCart}>加入购物车</button>
      <Link to="/products">返回商品列表</Link>
    </div>
  );
}

// 购物车
function Cart() {
  const navigate = useNavigate();
  const cartItems = getCartItems();

  const handleCheckout = () => {
    navigate('/checkout');
  };

  return (
    <div>
      <h1>购物车</h1>
      {cartItems.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartItems.map(item => (
            <div key={item.id}>
              <h3>{item.name}</h3>
              <p>${item.price} × {item.quantity}</p>
            </div>
          ))}
          <div>总计: ${calculateTotal()}</div>
          <button onClick={handleCheckout}>去结账</button>
        </>
      )}
      <Link to="/products">继续购物</Link>
    </div>
  );
}

// 结账
function Checkout() {
  const navigate = useNavigate();

  const handleSubmitOrder = () => {
    // 处理订单提交
    navigate('/confirmation');
  };

  return (
    <div>
      <h1>结账</h1>
      <CheckoutForm onSubmit={handleSubmitOrder} />
    </div>
  );
}

// 订单确认
function Confirmation() {
  return (
    <div>
      <h1>订单确认</h1>
      <p>您的订单已成功提交!</p>
      <Link to="/products">继续购物</Link>
    </div>
  );
}

// 应用路由
function App() {
  return (
    <div>
      <nav>
        <Link to="/">首页</Link> |
        <Link to="/products">商品</Link> |
        <Link to="/cart">购物车</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<ProductList />} />
        <Route path="/product/:id" element={<ProductDetail />} />
        <Route path="/cart" element={<Cart />} />
        <Route path="/checkout" element={<Checkout />} />
        <Route path="/confirmation" element={<Confirmation />} />
      </Routes>
    </div>
  );
}

3. 博客系统

import { Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';

// 博客首页
function BlogHome() {
  return (
    <div>
      <h1>我的博客</h1>
      <div className="blog-posts">
        {posts.map(post => (
          <div key={post.id} className="blog-post-preview">
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
            <Link to={`/blog/${post.slug}`}>阅读更多</Link>
          </div>
        ))}
      </div>
    </div>
  );
}

// 博客文章详情
function BlogPost() {
  const { slug } = useParams();
  const post = posts.find(p => p.slug === slug);

  if (!post) {
    return <div>文章不存在</div>;
  }

  return (
    <div className="blog-post">
      <h1>{post.title}</h1>
      <p className="post-meta">{post.date} · {post.author}</p>
      <div className="post-content">{post.content}</div>
      <Link to="/blog">返回博客首页</Link>
    </div>
  );
}

// 博客管理
function BlogAdmin() {
  const navigate = useNavigate();

  const handleCreatePost = () => {
    navigate('/blog/admin/create');
  };

  const handleEditPost = (postId) => {
    navigate(`/blog/admin/edit/${postId}`);
  };

  return (
    <div>
      <h1>博客管理</h1>
      <button onClick={handleCreatePost}>创建新文章</button>
      <div className="post-list">
        {posts.map(post => (
          <div key={post.id}>
            <h3>{post.title}</h3>
            <button onClick={() => handleEditPost(post.id)}>编辑</button>
          </div>
        ))}
      </div>
    </div>
  );
}

// 应用路由
function App() {
  return (
    <div>
      <nav>
        <Link to="/">首页</Link> |
        <Link to="/blog">博客</Link> |
        <Link to="/blog/admin">博客管理</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/blog" element={<BlogHome />} />
        <Route path="/blog/:slug" element={<BlogPost />} />
        <Route path="/blog/admin" element={<BlogAdmin />} />
        <Route path="/blog/admin/create" element={<CreatePost />} />
        <Route path="/blog/admin/edit/:id" element={<EditPost />} />
      </Routes>
    </div>
  );
}

代码优化建议

  1. 路由配置分离:将路由配置分离到单独的文件中,按功能模块组织
  2. 使用懒加载:对大型路由组件使用 React.lazy() 和 Suspense 实现懒加载
  3. 导航守卫抽象:将认证逻辑抽象为可重用的高阶组件或钩子
  4. 嵌套路由合理使用:利用嵌套路由和布局组件减少代码重复
  5. 参数验证:对路由参数进行验证,确保数据安全性
  6. 错误边界:为路由组件添加错误边界,提高应用稳定性
  7. 导航状态管理:合理使用 location.state 传递临时数据,减少全局状态依赖
  8. SEO 考虑:对于需要 SEO 的页面,考虑使用服务端渲染或静态生成
  9. 性能优化
    • 避免在路由组件中进行昂贵的计算
    • 使用 memo 缓存路由组件
    • 合理使用 useMemo 和 useCallback
    • 优化导航守卫的执行时间
  10. 代码组织
    • 按功能模块组织路由和组件
    • 使用一致的命名规范
    • 添加适当的注释
    • 使用 TypeScript 提高类型安全性

参考资源

« 上一篇 Vue Router 教程 - Vue.js 的官方路由 下一篇 » Redux Toolkit 教程 - Redux 的官方工具集