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