Skip to content

路由规范

核心原则

必须使用 React Router v7 作为路由方案,遵循声明式路由配置,禁止使用其他路由库或手动操作浏览器历史。

路由配置

基础配置

typescript
// src/router/index.tsx
import { createBrowserRouter, RouterProvider } from 'react-router';
import RootLayout from '@/layouts/RootLayout';
import HomePage from '@/pages/Home';
import NotFoundPage from '@/pages/NotFound';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <HomePage /> },
      { path: '*', element: <NotFoundPage /> },
    ],
  },
]);

export default function AppRouter() {
  return <RouterProvider router={router} />;
}

入口文件配置

typescript
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import AppRouter from '@/router';
import '@/index.css';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <AppRouter />
  </StrictMode>
);

嵌套路由与布局

布局组件

typescript
// src/layouts/RootLayout.tsx
import { Outlet } from 'react-router';
import { Header } from '@/components/Header';
import { Sidebar } from '@/components/Sidebar';

export default function RootLayout() {
  return (
    <div className="flex min-h-screen">
      <Sidebar />
      <div className="flex flex-1 flex-col">
        <Header />
        <main className="flex-1 p-6">
          <Outlet />
        </main>
      </div>
    </div>
  );
}

嵌套路由配置

typescript
// src/router/index.tsx
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <HomePage /> },
      {
        path: 'users',
        element: <UsersLayout />,
        children: [
          { index: true, element: <UserListPage /> },
          { path: ':id', element: <UserDetailPage /> },
          { path: ':id/edit', element: <UserEditPage /> },
        ],
      },
      {
        path: 'settings',
        element: <SettingsLayout />,
        children: [
          { index: true, element: <SettingsGeneralPage /> },
          { path: 'profile', element: <SettingsProfilePage /> },
          { path: 'security', element: <SettingsSecurityPage /> },
        ],
      },
    ],
  },
]);

动态路由参数

参数获取

typescript
// src/pages/users/[id].tsx
import { useParams } from 'react-router';

interface UserDetailParams {
  id: string;
}

export default function UserDetailPage() {
  const { id } = useParams<UserDetailParams>();

  return (
    <div>
      <h1>用户详情: {id}</h1>
    </div>
  );
}

查询参数

typescript
// src/pages/users/index.tsx
import { useSearchParams } from 'react-router';

export default function UserListPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const page = searchParams.get('page') ?? '1';
  const keyword = searchParams.get('keyword') ?? '';

  const handleSearch = (value: string) => {
    setSearchParams({ keyword: value, page: '1' });
  };

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="搜索用户"
      />
    </div>
  );
}

路由守卫

认证守卫

typescript
// src/components/AuthGuard.tsx
import { Navigate, useLocation } from 'react-router';
import { useAuthStore } from '@/stores/auth';

interface AuthGuardProps {
  children: React.ReactNode;
}

export function AuthGuard({ children }: AuthGuardProps) {
  const { isAuthenticated } = useAuthStore();
  const location = useLocation();

  if (!isAuthenticated) {
    // 保存当前位置,登录后重定向回来
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <>{children}</>;
}

在路由中使用守卫

typescript
// src/router/index.tsx
import { AuthGuard } from '@/components/AuthGuard';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <HomePage /> },
      { path: 'login', element: <LoginPage /> },
      {
        path: 'dashboard',
        element: (
          <AuthGuard>
            <DashboardPage />
          </AuthGuard>
        ),
      },
      {
        path: 'admin',
        element: (
          <AuthGuard>
            <AdminLayout />
          </AuthGuard>
        ),
        children: [
          { index: true, element: <AdminDashboard /> },
          { path: 'users', element: <AdminUsersPage /> },
        ],
      },
    ],
  },
]);

权限守卫

typescript
// src/components/PermissionGuard.tsx
import { Navigate } from 'react-router';
import { useAuthStore } from '@/stores/auth';

interface PermissionGuardProps {
  children: React.ReactNode;
  requiredRoles: string[];
}

export function PermissionGuard({ children, requiredRoles }: PermissionGuardProps) {
  const { user } = useAuthStore();

  const hasPermission = requiredRoles.some((role) => user?.roles.includes(role));

  if (!hasPermission) {
    return <Navigate to="/403" replace />;
  }

  return <>{children}</>;
}

代码分割与懒加载

路由级懒加载

typescript
// src/router/index.tsx
import { createBrowserRouter } from 'react-router';
import { lazy, Suspense } from 'react';
import RootLayout from '@/layouts/RootLayout';
import { LoadingSpinner } from '@/components/ui/loading-spinner';

// 懒加载页面组件
const HomePage = lazy(() => import('@/pages/Home'));
const UserListPage = lazy(() => import('@/pages/users'));
const UserDetailPage = lazy(() => import('@/pages/users/[id]'));
const SettingsPage = lazy(() => import('@/pages/settings'));

// 懒加载包装组件
function LazyPage({ component: Component }: { component: React.LazyExoticComponent<() => JSX.Element> }) {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Component />
    </Suspense>
  );
}

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <LazyPage component={HomePage} /> },
      { path: 'users', element: <LazyPage component={UserListPage} /> },
      { path: 'users/:id', element: <LazyPage component={UserDetailPage} /> },
      { path: 'settings', element: <LazyPage component={SettingsPage} /> },
    ],
  },
]);

使用 route.lazy 属性

typescript
// src/router/index.tsx(React Router v7 推荐方式)
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, lazy: () => import('@/pages/Home') },
      { path: 'users', lazy: () => import('@/pages/users') },
      { path: 'users/:id', lazy: () => import('@/pages/users/[id]') },
    ],
  },
]);

导航与跳转

声明式导航

typescript
import { Link, NavLink } from 'react-router';

function Navigation() {
  return (
    <nav className="flex gap-4">
      {/* 基础链接 */}
      <Link to="/">首页</Link>

      {/* 带活跃状态的导航链接 */}
      <NavLink
        to="/users"
        className={({ isActive }) =>
          isActive ? 'text-primary font-bold' : 'text-muted-foreground'
        }
      >
        用户管理
      </NavLink>

      {/* 带参数的链接 */}
      <Link to={`/users/${userId}`}>用户详情</Link>
    </nav>
  );
}

编程式导航

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

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

  const handleEdit = (id: string) => {
    navigate(`/users/${id}/edit`);
  };

  const handleBack = () => {
    navigate(-1); // 返回上一页
  };

  const handleLoginSuccess = () => {
    // 跳转并替换当前历史记录
    navigate('/dashboard', { replace: true });
  };

  const handleNavigateWithState = () => {
    // 携带状态跳转
    navigate('/checkout', { state: { from: 'cart' } });
  };

  return (
    <div className="flex gap-2">
      <Button onClick={() => handleEdit('123')}>编辑</Button>
      <Button variant="outline" onClick={handleBack}>返回</Button>
    </div>
  );
}

错误处理

错误边界

typescript
// src/router/index.tsx
import { ErrorBoundary } from '@/components/ErrorBoundary';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <ErrorBoundary />,
    children: [
      // ...
    ],
  },
]);

错误边界组件

typescript
// src/components/ErrorBoundary.tsx
import { useRouteError, isRouteErrorResponse, Link } from 'react-router';
import { Button } from '@/components/ui/button';

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div className="flex flex-col items-center justify-center min-h-screen">
        <h1 className="text-4xl font-bold">{error.status}</h1>
        <p className="text-muted-foreground mt-2">{error.statusText}</p>
        <Button asChild className="mt-4">
          <Link to="/">返回首页</Link>
        </Button>
      </div>
    );
  }

  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h1 className="text-4xl font-bold">出错了</h1>
      <p className="text-muted-foreground mt-2">请稍后重试</p>
      <Button asChild className="mt-4">
        <Link to="/">返回首页</Link>
      </Button>
    </div>
  );
}

禁止的做法

  • 禁止使用旧版 <BrowserRouter> 组件式 API
  • 禁止直接操作 window.location 进行跳转
  • 禁止在路由配置中硬编码用户信息
  • 禁止省略路由守卫(对于需要认证的页面)
  • 禁止在大型应用中不使用代码分割
  • 禁止使用其他路由库(如 @tanstack/router、wouter)