Skip to content

动态路由

🙋 博客文章、用户主页、商品详情…这些路径不可能逐个手写。动态路由如何解决这个问题?

动态路由概述#

动态路由使用方括号 [] 语法定义参数化路径:

语法示例匹配
[slug]/blog/[slug]/blog/hello
[...slug]/docs/[...slug]/docs/a/b/c
[[...slug]]/[[...slug]]//a/b/c

单参数动态路由 [param]#

基础用法#

app/
└── blog/
└── [slug]/
└── page.tsx # /blog/:slug
app/blog/[slug]/page.tsx
// Next.js 15.x
interface PageProps {
params: Promise<{ slug: string }>
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
// 根据 slug 获取文章数据
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
async function getPost(slug: string) {
// 模拟数据获取
return {
title: `文章: ${slug}`,
content: '这是文章内容...',
}
}

访问 /blog/hello-worldslug 值为 "hello-world"

多个动态参数#

app/
└── [category]/
└── [productId]/
└── page.tsx # /:category/:productId
app/[category]/[productId]/page.tsx
// Next.js 15.x
interface PageProps {
params: Promise<{
category: string
productId: string
}>
}
export default async function ProductPage({ params }: PageProps) {
const { category, productId } = await params
return (
<div>
<p>分类: {category}</p>
<p>产品 ID: {productId}</p>
</div>
)
}

访问 /electronics/iphone-15

生成静态路径#

使用 generateStaticParams 预生成静态页面:

app/blog/[slug]/page.tsx
// Next.js 15.x
// 构建时生成这些路径的静态页面
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
interface PageProps {
params: Promise<{ slug: string }>
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.title}</article>
}

🎯 效果:构建时为每篇文章生成静态 HTML,访问速度极快。

捕获所有路由 […slug]#

基础用法#

捕获路径中的所有段:

app/
└── docs/
└── [...slug]/
└── page.tsx # /docs/*
app/docs/[...slug]/page.tsx
// Next.js 15.x
interface PageProps {
params: Promise<{ slug: string[] }>
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params
// slug 是数组
return (
<div>
<p>路径段: {slug.join(' / ')}</p>
<p>段数量: {slug.length}</p>
</div>
)
}

匹配示例:

URLslug 值
/docs/intro["intro"]
/docs/getting-started/installation["getting-started", "installation"]
/docs/api/v2/users["api", "v2", "users"]
/docs❌ 不匹配

🔶 注意[...slug] 不匹配父路径 /docs

文档网站示例#

app/docs/[...slug]/page.tsx
// Next.js 15.x
import { notFound } from 'next/navigation'
// 模拟文档结构
const docs: Record<string, { title: string; content: string }> = {
'intro': {
title: '简介',
content: '欢迎使用我们的文档...',
},
'getting-started/installation': {
title: '安装',
content: '运行 npm install...',
},
'api/reference': {
title: 'API 参考',
content: 'API 文档...',
},
}
interface PageProps {
params: Promise<{ slug: string[] }>
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params
const path = slug.join('/')
const doc = docs[path]
if (!doc) {
notFound()
}
return (
<article>
<nav className="text-sm text-gray-500 mb-4">
{slug.map((segment, i) => (
<span key={i}>
{i > 0 && ' / '}
{segment}
</span>
))}
</nav>
<h1 className="text-2xl font-bold">{doc.title}</h1>
<p className="mt-4">{doc.content}</p>
</article>
)
}

生成静态路径#

app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
return [
{ slug: ['intro'] },
{ slug: ['getting-started', 'installation'] },
{ slug: ['getting-started', 'configuration'] },
{ slug: ['api', 'reference'] },
{ slug: ['api', 'examples'] },
]
}

可选捕获所有路由 [[…slug]]#

基础用法#

双方括号使捕获变为可选,可以匹配父路径:

app/
└── [[...slug]]/
└── page.tsx # / 和 /*
app/[[...slug]]/page.tsx
// Next.js 15.x
interface PageProps {
params: Promise<{ slug?: string[] }>
}
export default async function CatchAllPage({ params }: PageProps) {
const { slug } = await params
if (!slug) {
return <h1>首页</h1>
}
return <h1>路径: /{slug.join('/')}</h1>
}

匹配示例:

URLslug 值
/undefined
/about["about"]
/blog/2024/post["blog", "2024", "post"]

SPA 降级模式#

可选捕获常用于 SPA 降级或旧站点迁移:

app/[[...slug]]/page.tsx
// Next.js 15.x
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
export default function SPAFallback() {
const pathname = usePathname()
useEffect(() => {
// 客户端路由处理
console.log('当前路径:', pathname)
}, [pathname])
return (
<div>
<h1>SPA 应用</h1>
{/* 客户端路由逻辑 */}
</div>
)
}

获取 URL 参数#

查询参数 searchParams#

app/search/page.tsx
// Next.js 15.x
interface PageProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export default async function SearchPage({ searchParams }: PageProps) {
const { q, page, category } = await searchParams
return (
<div>
<p>搜索词: {q}</p>
<p>页码: {page ?? '1'}</p>
<p>分类: {Array.isArray(category) ? category.join(', ') : category}</p>
</div>
)
}

访问 /search?q=next&page=2&category=docs&category=blog

客户端获取参数#

components/SearchBox.tsx
// Next.js 15.x
'use client'
import { useSearchParams, usePathname, useRouter } from 'next/navigation'
export default function SearchBox() {
const searchParams = useSearchParams()
const pathname = usePathname()
const router = useRouter()
const query = searchParams.get('q') ?? ''
const handleSearch = (term: string) => {
const params = new URLSearchParams(searchParams.toString())
if (term) {
params.set('q', term)
} else {
params.delete('q')
}
router.push(`${pathname}?${params.toString()}`)
}
return (
<input
type="search"
defaultValue={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="搜索..."
/>
)
}

动态元数据#

基于参数生成元数据#

app/blog/[slug]/page.tsx
// Next.js 15.x
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.coverImage],
},
}
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}

路由优先级#

当多个路由可能匹配时,Next.js 按以下优先级选择:

  1. 静态路由 > 动态路由
  2. 具体路由 > 捕获所有路由
app/
├── blog/
│ ├── page.tsx # /blog(优先)
│ ├── featured/
│ │ └── page.tsx # /blog/featured(优先)
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── [...slug]/
└── page.tsx # 其他所有路径
URL匹配
/blogblog/page.tsx
/blog/featuredblog/featured/page.tsx
/blog/helloblog/[slug]/page.tsx
/anything/else[...slug]/page.tsx

常见问题#

🤔 Q: 动态路由可以有默认值吗?

不能直接设置默认值,但可以在组件中处理:

export default async function Page({ params }) {
const { slug = 'default' } = await params
return <div>{slug}</div>
}

🤔 Q: 如何验证动态参数?

import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const { id } = await params
// 验证 ID 格式
if (!/^\d+$/.test(id)) {
notFound()
}
// 验证数据存在
const data = await getData(id)
if (!data) {
notFound()
}
return <div>{data.name}</div>
}

🤔 Q: 动态路由和 API 路由可以同路径吗?

不行。page.tsxroute.ts 不能在同一目录:

app/
└── api/
└── users/
├── page.tsx # ❌ 冲突
└── route.ts # ❌ 冲突

下一篇将深入嵌套路由与布局,探索如何构建复杂的页面结构。

-EOF-