路由规范
核心原则
必须使用 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)