跳到主要内容

路由系统

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 组件进行客户端导航:

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 实现:

  1. 页面开始加载时显示 loading 组件
  2. 页面内容加载完成后替换为实际内容
  3. 导航时自动显示加载状态

错误处理

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>;
}

小结

本章我们学习了:

  1. 文件系统路由的基本概念
  2. 静态路由、动态路由、捕获所有路由
  3. 布局嵌套和路由组
  4. Link 组件和编程式导航
  5. 查询参数的处理
  6. 加载状态和错误处理
  7. 自定义 404 页面

练习

  1. 创建一个包含首页、关于、联系三个静态路由的网站
  2. 实现一个动态路由 /users/[id],显示用户 ID
  3. 创建一个嵌套布局,包含侧边栏导航
  4. 实现一个搜索页面,支持查询参数
  5. 创建自定义的加载状态和错误页面