Next.js动态路由 #
一、动态路由基础 #
1.1 动态段语法 #
使用方括号 [param] 创建动态路由段:
text
app/
├── blog/
│ └── [slug]/
│ └── page.tsx → /blog/:slug
├── shop/
│ └── [category]/
│ └── [id]/
│ └── page.tsx → /shop/:category/:id
└── users/
└── [id]/
└── page.tsx → /users/:id
1.2 获取动态参数 #
tsx
interface PageProps {
params: Promise<{ slug: string }>
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
return <h1>文章: {slug}</h1>
}
1.3 参数类型 #
tsx
interface PageProps {
params: Promise<{
category: string
id: string
}>
searchParams: Promise<{
page?: string
sort?: string
}>
}
export default async function ProductPage({
params,
searchParams,
}: PageProps) {
const { category, id } = await params
const { page = '1', sort = 'asc' } = await searchParams
return (
<div>
<p>分类: {category}</p>
<p>ID: {id}</p>
<p>页码: {page}</p>
<p>排序: {sort}</p>
</div>
)
}
二、捕获所有路由 #
2.1 必选捕获 #
使用 [...slug] 捕获所有后续段:
text
app/
└── docs/
└── [...slug]/
└── page.tsx
URL映射:
text
/docs/a → slug: ['a']
/docs/a/b → slug: ['a', 'b']
/docs/a/b/c → slug: ['a', 'b', 'c']
页面实现:
tsx
interface PageProps {
params: Promise<{ slug: string[] }>
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params
const path = slug.join('/')
const doc = await getDoc(path)
return (
<article>
<h1>{doc.title}</h1>
<p>路径: {path}</p>
<div>{doc.content}</div>
</article>
)
}
2.2 可选捕获 #
使用 [[...slug]] 创建可选捕获路由:
text
app/
└── docs/
└── [[...slug]]/
└── page.tsx
URL映射:
text
/docs → slug: undefined (或 [])
/docs/a → slug: ['a']
/docs/a/b → slug: ['a', 'b']
页面实现:
tsx
interface PageProps {
params: Promise<{ slug?: string[] }>
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params
if (!slug || slug.length === 0) {
return <DocsIndex />
}
const path = slug.join('/')
const doc = await getDoc(path)
return <DocContent doc={doc} />
}
三、generateStaticParams #
3.1 基本用法 #
预生成动态路由的静态页面:
tsx
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}
3.2 多参数生成 #
tsx
interface PageProps {
params: Promise<{
category: string
id: string
}>
}
export async function generateStaticParams() {
const products = await getProducts()
return products.map((product) => ({
category: product.category,
id: product.id.toString(),
}))
}
export default async function ProductPage({ params }: PageProps) {
const { category, id } = await params
const product = await getProduct(category, id)
return <ProductDetails product={product} />
}
3.3 捕获所有路由生成 #
tsx
interface PageProps {
params: Promise<{ slug: string[] }>
}
export async function generateStaticParams() {
const docs = await getAllDocs()
return docs.map((doc) => ({
slug: doc.path.split('/'),
}))
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params
const doc = await getDoc(slug.join('/'))
return <DocContent doc={doc} />
}
3.4 动态参数配置 #
tsx
export const dynamicParams = true
export async function generateStaticParams() {
const popularPosts = await getPopularPosts()
return popularPosts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}
| 配置 | 说明 |
|---|---|
dynamicParams: true |
允许访问未预生成的动态路由 |
dynamicParams: false |
只允许访问预生成的路由 |
四、路由参数处理 #
4.1 参数验证 #
tsx
import { notFound } from 'next/navigation'
interface PageProps {
params: Promise<{ id: string }>
}
export default async function UserPage({ params }: PageProps) {
const { id } = await params
if (!isValidId(id)) {
notFound()
}
const user = await getUser(id)
if (!user) {
notFound()
}
return <UserProfile user={user} />
}
4.2 参数转换 #
tsx
interface PageProps {
params: Promise<{ id: string }>
}
export default async function ProductPage({ params }: PageProps) {
const { id } = await params
const productId = parseInt(id, 10)
if (isNaN(productId)) {
notFound()
}
const product = await getProductById(productId)
return <ProductDetails product={product} />
}
4.3 参数解码 #
tsx
interface PageProps {
params: Promise<{ slug: string }>
}
export default async function Page({ params }: PageProps) {
const { slug } = await params
const decodedSlug = decodeURIComponent(slug)
const post = await getPost(decodedSlug)
return <article>{post.content}</article>
}
五、动态路由布局 #
5.1 动态布局 #
tsx
interface LayoutProps {
children: React.ReactNode
params: Promise<{ category: string }>
}
export default async function CategoryLayout({
children,
params,
}: LayoutProps) {
const { category } = await params
const categoryInfo = await getCategoryInfo(category)
return (
<div>
<header>
<h1>{categoryInfo.name}</h1>
<p>{categoryInfo.description}</p>
</header>
<main>{children}</main>
</div>
)
}
5.2 嵌套动态路由 #
text
app/
└── categories/
└── [category]/
├── layout.tsx
├── page.tsx
└── products/
└── [productId]/
└── page.tsx
tsx
interface PageProps {
params: Promise<{
category: string
productId: string
}>
}
export default async function ProductPage({ params }: PageProps) {
const { category, productId } = await params
const product = await getProduct(category, productId)
return <ProductDetails product={product} />
}
六、动态路由与SEO #
6.1 动态元数据 #
tsx
import type { Metadata } from 'next'
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}
6.2 动态站点地图 #
tsx
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts()
const postUrls = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postUrls,
]
}
七、实战案例 #
7.1 博客系统 #
tsx
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: `${post.title} | 我的博客`,
description: post.excerpt,
}
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
return (
<article className="max-w-3xl mx-auto">
<header className="mb-8">
<h1 className="text-4xl font-bold">{post.title}</h1>
<time className="text-gray-500">{post.date}</time>
</header>
<div className="prose">{post.content}</div>
<footer className="mt-8">
<Tags tags={post.tags} />
<AuthorCard author={post.author} />
</footer>
</article>
)
}
7.2 电商产品页 #
tsx
interface PageProps {
params: Promise<{
category: string
productId: string
}>
}
export async function generateStaticParams() {
const products = await getProducts()
return products.map((product) => ({
category: product.category,
productId: product.id.toString(),
}))
}
export default async function ProductPage({ params }: PageProps) {
const { category, productId } = await params
const product = await getProduct(category, productId)
const relatedProducts = await getRelatedProducts(product.id)
return (
<div className="container mx-auto">
<ProductHeader product={product} />
<div className="grid md:grid-cols-2 gap-8">
<ProductGallery images={product.images} />
<ProductInfo product={product} />
</div>
<ProductTabs product={product} />
<RelatedProducts products={relatedProducts} />
</div>
)
}
7.3 文档系统 #
tsx
interface PageProps {
params: Promise<{ slug?: string[] }>
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params
if (!slug || slug.length === 0) {
return <DocsHomePage />
}
const path = slug.join('/')
const doc = await getDoc(path)
if (!doc) {
notFound()
}
return (
<div className="flex">
<Sidebar currentPath={path} />
<main className="flex-1">
<Breadcrumbs path={slug} />
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.html }} />
</article>
<Pagination prev={doc.prev} next={doc.next} />
</main>
<TOC headings={doc.headings} />
</div>
)
}
八、最佳实践 #
8.1 参数类型安全 #
tsx
import { z } from 'zod'
const paramsSchema = z.object({
id: z.string().uuid(),
})
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const rawParams = await params
const result = paramsSchema.safeParse(rawParams)
if (!result.success) {
notFound()
}
const { id } = result.data
const item = await getItem(id)
return <ItemDetails item={item} />
}
8.2 错误处理 #
tsx
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
try {
const { id } = await params
const data = await getData(id)
return <Content data={data} />
} catch (error) {
if (error instanceof NotFoundError) {
notFound()
}
throw error
}
}
8.3 缓存策略 #
tsx
export const revalidate = 3600
export async function generateStaticParams() {
return getStaticPaths()
}
export default async function Page({ params }: PageProps) {
const { slug } = await params
const data = await getData(slug, { next: { revalidate: 60 } })
return <Content data={data} />
}
九、总结 #
动态路由要点:
| 要点 | 说明 |
|---|---|
| 动态段 | [param] 语法 |
| 捕获所有 | [...slug] 语法 |
| 可选捕获 | [[...slug]] 语法 |
| 静态生成 | generateStaticParams |
| 参数获取 | params prop |
| 类型安全 | TypeScript接口 |
下一步,让我们学习路由组与并行路由!
最后更新:2026-03-28