路由系统
Next.js 使用基于文件系统的路由,通过文件夹和文件来定义路由结构。本章将详细介绍 App Router 的路由机制。
路由基础
文件系统路由
在 App Router 中,路由由文件夹结构决定:
- 文件夹:定义路由路径
- 文件:定义路由 UI
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
页面文件
page.tsx 文件定义路由页面,必须默认导出一个 React 组件:
// src/app/about/page.tsx
export default function AboutPage() {
return <div>关于我们</div>;
}
访问 /about 路由将渲染此组件。
布局文件
layout.tsx 文件定义共享布局,必须接受 children 属性:
// src/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<header>网站头部</header>
{children}
<footer>网站底部</footer>
</body>
</html>
);
}
路由类型
静态路由
最简单的路由形式,路径固定:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── contact/
└── page.tsx → /contact
动态路由
使用方括号 [参数名] 创建动态路由:
// src/app/blog/[slug]/page.tsx
export default function BlogPost({
params,
}: {
params: { slug: string };
}) {
return <div>文章: {params.slug}</div>;
}
访问 /blog/hello-world 时,params.slug 为 "hello-world"。
动态路由生成
使用 generateStaticParams 在构建时生成静态页面:
// src/app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default function BlogPost({
params,
}: {
params: { slug: string };
}) {
return <div>文章: {params.slug}</div>;
}
捕获所有路由
使用 [...slug] 捕获多级路径:
// src/app/docs/[...slug]/page.tsx
export default function DocsPage({
params,
}: {
params: { slug: string[] };
}) {
return <div>文档路径: {params.slug.join('/')}</div>;
}
访问 /docs/a/b/c 时,params.slug 为 ['a', 'b', 'c']。
可选捕获所有路由
使用 [[...slug]] 创建可选的捕获所有路由:
// src/app/docs/[[...slug]]/page.tsx
可以匹配 /docs、/docs/a、/docs/a/b 等路径。
布局嵌套
嵌套布局
布局可以嵌套,子布局会包裹在父布局内:
app/
├── layout.tsx → 根布局
├── page.tsx → 首页
└── dashboard/
├── layout.tsx → dashboard 布局
└── page.tsx → /dashboard 页面
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<nav>侧边栏</nav>
<main>{children}</main>
</div>
);
}
布局层级
最终的 HTML 结构:
<html>
<body>
<!-- 根布局内容 -->
<div class="flex">
<nav>侧边栏</nav>
<main>
<!-- dashboard 页面内容 -->
</main>
</div>
</body>
</html>
路由组
使用 (组名) 创建路由组,不影响 URL 路径:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about
│ └── contact/
│ └── page.tsx → /contact
└── (shop)/
├── products/
│ └── page.tsx → /products
└── cart/
└── page.tsx → /cart
路由组的作用:
- 组织代码结构
- 共享布局
- 不影响 URL 路径
页面导航
Link 组件
使用 Link 组件进行客户端导航:
import Link from "next/link";
export default function Navigation() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/blog/hello-world">文章</Link>
</nav>
);
}
动态链接
import Link from "next/link";
export default function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</li>
))}
</ul>
);
}
编程式导航
使用 useRouter Hook 进行编程式导航:
"use client";
import { useRouter } from "next/navigation";
export default function LoginForm() {
const router = useRouter();
const handleSubmit = async (formData: FormData) => {
const success = await login(formData);
if (success) {
router.push("/dashboard");
}
};
return (
<form action={handleSubmit}>
<button type="submit">登录</button>
</form>
);
}
useRouter 方法
const router = useRouter();
// 导航到新页面
router.push("/path");
// 替换当前历史记录
router.replace("/path");
// 返回上一页
router.back();
// 前进
router.forward();
// 刷新当前页面
router.refresh();
查询参数
获取查询参数
在 Server Component 中使用 searchParams:
// src/app/search/page.tsx
export default function SearchPage({
searchParams,
}: {
searchParams: { q?: string; page?: string };
}) {
const query = searchParams.q || "";
const page = Number(searchParams.page) || 1;
return (
<div>
搜索: {query},第 {page} 页
</div>
);
}
客户端获取查询参数
在 Client Component 中使用 useSearchParams:
"use client";
import { useSearchParams } from "next/navigation";
export default function SearchFilters() {
const searchParams = useSearchParams();
const category = searchParams.get("category");
const sort = searchParams.get("sort");
return (
<div>
分类: {category},排序: {sort}
</div>
);
}
修改查询参数
"use client";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
export default function Pagination() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const setPage = (page: number) => {
const params = new URLSearchParams(searchParams);
params.set("page", String(page));
router.push(`${pathname}?${params.toString()}`);
};
return (
<div>
<button onClick={() => setPage(1)}>第1页</button>
<button onClick={() => setPage(2)}>第2页</button>
</div>
);
}
加载状态
loading.tsx
创建 loading.tsx 文件定义加载状态:
// src/app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
);
}
工作原理
loading.tsx 基于 React Suspense 实现:
- 页面开始加载时显示 loading 组件
- 页面内容加载完成后替换为实际内容
- 导航时自动显示加载状态
错误处理
error.tsx
创建 error.tsx 文件处理错误:
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}
global-error.tsx
处理根布局错误的特殊文件:
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>全局错误</h2>
<button onClick={reset}>重试</button>
</body>
</html>
);
}
404 页面
not-found.tsx
创建自定义 404 页面:
// src/app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h2>页面未找到</h2>
<p>您访问的页面不存在</p>
<Link href="/">返回首页</Link>
</div>
);
}
触发 404
使用 notFound() 函数主动触发 404:
import { notFound } from "next/navigation";
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return <article>{post.content}</article>;
}
小结
本章我们学习了:
- 文件系统路由的基本概念
- 静态路由、动态路由、捕获所有路由
- 布局嵌套和路由组
- Link 组件和编程式导航
- 查询参数的处理
- 加载状态和错误处理
- 自定义 404 页面
练习
- 创建一个包含首页、关于、联系三个静态路由的网站
- 实现一个动态路由
/users/[id],显示用户 ID - 创建一个嵌套布局,包含侧边栏导航
- 实现一个搜索页面,支持查询参数
- 创建自定义的加载状态和错误页面