📚 现代前端开发 目录
阶段3:React
第 18 / 35 课

React Router

单页应用的路由方案

📖 核心概念

💻 代码实现

React Router配置 ✅ tsx
import {
  createBrowserRouter, RouterProvider,
  Outlet, Link, NavLink, useNavigate,
  useParams, useSearchParams, useLocation,
  Navigate, useLoaderData,
} from 'react-router-dom';
import { lazy, Suspense } from 'react';

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

// 布局组件
function RootLayout() {
  return (
    <div className="app">
      <nav className="sidebar">
        <NavLink to="/" end className={({ isActive }) => isActive ? 'active' : ''}>首页</NavLink>
        <NavLink to="/about">关于</NavLink>
        <NavLink to="/dashboard">控制台</NavLink>
      </nav>
      <main className="content">
        <Suspense fallback={<div>加载中...</div>}>
          <Outlet />
        </Suspense>
      </main>
    </div>
  );
}

// 路由守卫
function AuthGuard({ children }: { children: ReactNode }) {
  const token = useStore(s => s.token);
  const location = useLocation();

  if (!token) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return <>{children}</>;
}

// 路由配置
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      {
        path: 'dashboard',
        element: <AuthGuard><Dashboard /></AuthGuard>,
        children: [
          { index: true, element: <DashboardHome /> },
          { path: 'settings', element: <Settings /> },
          { path: 'users/:userId', element: <UserProfile />,
            loader: async ({ params }) => {
              const user = await fetchUser(params.userId!);
              return user;
            },
          },
        ],
      },
      { path: 'login', element: <Login /> },
      { path: '*', element: <NotFound /> },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}
高级路由模式 ✅ tsx
// 路由参数与搜索参数
function ProductPage() {
  const { productId } = useParams(); // 动态参数
  const [searchParams, setSearchParams] = useSearchParams();
  const page = Number(searchParams.get('page') || 1);
  const sort = searchParams.get('sort') || 'latest';

  const { data } = useFetch(\`/api/products/\${productId}?page=\${page}&sort=\${sort}\`);

  return (
    <div>
      <h1>产品 {productId}</h1>
      {/* 修改搜索参数不会触发页面跳转 */}
      <select value={sort} onChange={e => {
        setSearchParams(prev => { prev.set('sort', e.target.value); return prev; });
      }}>
        <option value="latest">最新</option>
        <option value="price">价格</option>
      </select>
    </div>
  );
}

// 编程式导航
function LoginForm() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = (location.state as any)?.from?.pathname || '/';

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    try {
      await login(credentials);
      navigate(from, { replace: true }); // 返回之前的页面
    } catch (err) {
      setError('登录失败');
    }
  };
}

// 面包屑导航
function Breadcrumbs() {
  const location = useLocation();
  const pathSegments = location.pathname.split('/').filter(Boolean);

  const crumbs = pathSegments.map((segment, i) => {
    const path = '/' + pathSegments.slice(0, i + 1).join('/');
    const label = segment.charAt(0).toUpperCase() + segment.slice(1);
    return { path, label };
  });

  return (
    <nav className="breadcrumbs">
      <Link to="/">首页</Link>
      {crumbs.map(crumb => (
        <span key={crumb.path}>
          <span>/</span>
          <Link to={crumb.path}>{crumb.label}</Link>
        </span>
      ))}
    </nav>
  );
}

// 路由级别的数据加载
// 使用loader预加载数据,避免页面闪烁
const routes = [
  {
    path: '/blog',
    element: <BlogLayout />,
    loader: async () => {
      const posts = await fetchPosts();
      return { posts };
    },
    children: [
      {
        index: true,
        element: <PostList />,
        loader: async ({ request }) => {
          const url = new URL(request.url);
          const page = url.searchParams.get('page') || '1';
          return { page: Number(page) };
        },
      },
    ],
  },
];

🎯 练习任务

🏆 成就解锁
路由导航员 — 掌握React Router,构建流畅的单页应用导航体验