name: web-navigation
description: 用于React web应用程序的导航和路由模式。在实现React Router、Next.js路由、深度链接或处理导航状态时使用。
Web Navigation (React)
React Router (v6)
基本设置
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/users/:id" element={<UserDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}
嵌套路由和布局
// 带有共享UI的布局
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<main>
<Outlet /> {/* 子路由在这里渲染 */}
</main>
</div>
);
}
// 路由
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
动态路由
import { useParams, useSearchParams } from 'react-router-dom';
// 路由:/users/:id
function UserDetailPage() {
const { id } = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'profile';
return (
<div>
<h1>用户 {id}</h1>
<TabBar
active={tab}
onChange={(t) => setSearchParams({ tab: t })}
/>
</div>
);
}
程序化导航
import { useNavigate, useLocation } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
async function handleLogin() {
await login(credentials);
// 重定向到预期页面或默认
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
}
// 其他导航方法
navigate('/users'); // 推送到历史记录
navigate('/users', { replace: true }); // 替换当前条目
navigate(-1); // 返回
navigate(1); // 前进
}
链接组件
import { Link, NavLink } from 'react-router-dom';
// 基本链接
<Link to="/about">关于</Link>
// 带有状态
<Link to="/checkout" state={{ cartId: '123' }}>
结账
</Link>
// NavLink - 活动样式
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive ? 'nav-link active' : 'nav-link'
}
>
仪表盘
</NavLink>
Next.js App Router
文件基础路由
app/
├── layout.tsx # 根布局
├── page.tsx # / 路由
├── about/
│ └── page.tsx # /about 路由
├── users/
│ ├── page.tsx # /users 路由
│ └── [id]/
│ └── page.tsx # /users/:id 路由
├── (auth)/ # 路由组(无URL段)
│ ├── login/
│ │ └── page.tsx # /login 路由
│ └── register/
│ └── page.tsx # /register 路由
└── dashboard/
├── layout.tsx # 仪表盘布局
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
布局
// app/layout.tsx - 根布局
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
<Header />
{children}
<Footer />
</Providers>
</body>
</html>
);
}
// app/dashboard/layout.tsx - 嵌套布局
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
动态路由
// app/users/[id]/page.tsx
interface Props {
params: { id: string };
searchParams: { tab?: string };
}
export default function UserPage({ params, searchParams }: Props) {
const { id } = params;
const tab = searchParams.tab || 'profile';
return (
<div>
<h1>用户 {id}</h1>
<Tabs active={tab} />
</div>
);
}
// 生成静态参数(可选)
export async function generateStaticParams() {
const users = await getUsers();
return users.map((user) => ({
id: user.id,
}));
}
程序化导航
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
function SearchForm() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
function handleSearch(query: string) {
const params = new URLSearchParams(searchParams);
params.set('q', query);
router.push(`${pathname}?${params.toString()}`);
}
// 导航方法
router.push('/dashboard'); // 导航
router.replace('/dashboard'); // 替换无历史记录
router.back(); // 返回
router.forward(); // 前进
router.refresh(); // 刷新服务器组件
}
链接组件
import Link from 'next/link';
// 基本链接
<Link href="/about">关于</Link>
// 带有动态路由
<Link href={`/users/${user.id}`}>
{user.name}
</Link>
// 带有查询参数
<Link href={{ pathname: '/search', query: { q: 'react' } }}>
搜索
</Link>
// 预加载(默认:true)
<Link href="/dashboard" prefetch={false}>
仪表盘
</Link>
路由组和组织
受保护的vs公共路由
// React Router
<Routes>
{/* 公共路由 */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* 受保护的路由 */}
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>
</Routes>
// Next.js - 使用路由组
// app/(public)/login/page.tsx
// app/(protected)/dashboard/page.tsx
// app/(protected)/layout.tsx - 添加身份验证检查
加载和错误状态
React Router
import { Suspense } from 'react';
import { Await, useLoaderData, defer } from 'react-router-dom';
// 加载器
export async function loader({ params }) {
return defer({
user: getUser(params.id), // 承诺
});
}
// 组件
function UserPage() {
const { user } = useLoaderData();
return (
<Suspense fallback={<Spinner />}>
<Await resolve={user} errorElement={<ErrorFallback />}>
{(resolvedUser) => <UserProfile user={resolvedUser} />}
</Await>
</Suspense>
);
}
Next.js
// app/users/[id]/loading.tsx
export default function Loading() {
return <Spinner />;
}
// app/users/[id]/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>出了点问题!</h2>
<button onClick={reset}>再试一次</button>
</div>
);
}
// app/users/[id]/not-found.tsx
export default function NotFound() {
return <div>用户未找到</div>;
}
滚动恢复
React Router
import { ScrollRestoration } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>{/* ... */}</Routes>
<ScrollRestoration />
</BrowserRouter>
);
}
手动滚动到顶部
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
深度链接/查询参数
// 自定义钩子用于类型安全的查询参数
import { useSearchParams } from 'react-router-dom';
interface Filters {
category?: string;
sort?: 'asc' | 'desc';
page?: number;
}
function useFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const filters: Filters = {
category: searchParams.get('category') || undefined,
sort: (searchParams.get('sort') as 'asc' | 'desc') || undefined,
page: Number(searchParams.get('page')) || 1,
};
function setFilters(newFilters: Partial<Filters>) {
const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.set(key, String(value));
} else {
params.delete(key);
}
});
setSearchParams(params);
}
return { filters, setFilters };
}
// 使用
function ProductList() {
const { filters, setFilters } = useFilters();
return (
<div>
<CategorySelect
value={filters.category}
onChange={(cat) => setFilters({ category: cat })}
/>
<ProductGrid products={products} />
<Pagination
page={filters.page}
onChange={(p) => setFilters({ page: p })}
/>
</div>
);
}
导航守卫
// 防止带有未保存更改的导航
import { useBlocker } from 'react-router-dom';
function EditForm() {
const [isDirty, setIsDirty] = useState(false);
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
isDirty && currentLocation.pathname !== nextLocation.pathname
);
return (
<>
<form onChange={() => setIsDirty(true)}>
{/* 表单字段 */}
</form>
{blocker.state === 'blocked' && (
<ConfirmDialog
message="您有未保存的更改。仍然要离开吗?"
onConfirm={() => blocker.proceed()}
onCancel={() => blocker.reset()}
/>
)}
</>
);
}
常见模式
操作后的重定向
// 表单提交后
async function handleSubmit(data: FormData) {
const result = await createItem(data);
navigate(`/items/${result.id}`);
}
// 登录后
async function handleLogin() {
await login(credentials);
const redirectTo = searchParams.get('redirect') || '/dashboard';
navigate(redirectTo, { replace: true });
}
带有URL的标签导航
function UserProfile() {
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'overview';
const tabs = ['overview', 'activity', 'settings'];
return (
<div>
<nav>
{tabs.map((t) => (
<button
key={t}
onClick={() => setSearchParams({ tab: t })}
className={tab === t ? 'active' : ''}
>
{t}
</button>
))}
</nav>
<TabContent tab={tab} />
</div>
);
}
常见问题
| 问题 |
解决方案 |
| 路由不匹配 |
检查路由顺序(特定路由在动态路由之前) |
| 后退按钮不起作用 |
使用 navigate() 而不是 window.location |
| 刷新时状态丢失 |
将状态存储在URL参数中,而不仅仅是状态 |
| 滚动位置错误 |
添加 ScrollRestoration 组件 |
| 生产中的404(SPA) |
为SPA配置服务器 |