🙋 博客文章、用户主页、商品详情…这些路径不可能逐个手写。动态路由如何解决这个问题?
动态路由概述#
动态路由使用方括号 [] 语法定义参数化路径:
| 语法 | 示例 | 匹配 |
|---|---|---|
[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// 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-world,slug 值为 "hello-world"。
多个动态参数#
app/└── [category]/ └── [productId]/ └── page.tsx # /:category/:productId// 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:
category="electronics"productId="iphone-15"
生成静态路径#
使用 generateStaticParams 预生成静态页面:
// 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/*// 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> )}匹配示例:
| URL | slug 值 |
|---|---|
/docs/intro | ["intro"] |
/docs/getting-started/installation | ["getting-started", "installation"] |
/docs/api/v2/users | ["api", "v2", "users"] |
/docs | ❌ 不匹配 |
🔶 注意:[...slug] 不匹配父路径 /docs。
文档网站示例#
// Next.js 15.ximport { 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> )}生成静态路径#
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 # / 和 /*// 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>}匹配示例:
| URL | slug 值 |
|---|---|
/ | undefined |
/about | ["about"] |
/blog/2024/post | ["blog", "2024", "post"] |
SPA 降级模式#
可选捕获常用于 SPA 降级或旧站点迁移:
// 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#
// 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:
q="next"page="2"category=["docs", "blog"]
客户端获取参数#
// 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="搜索..." /> )}动态元数据#
基于参数生成元数据#
// Next.js 15.ximport 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 按以下优先级选择:
- 静态路由 > 动态路由
- 具体路由 > 捕获所有路由
app/├── blog/│ ├── page.tsx # /blog(优先)│ ├── featured/│ │ └── page.tsx # /blog/featured(优先)│ └── [slug]/│ └── page.tsx # /blog/:slug└── [...slug]/ └── page.tsx # 其他所有路径| URL | 匹配 |
|---|---|
/blog | blog/page.tsx |
/blog/featured | blog/featured/page.tsx |
/blog/hello | blog/[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.tsx 和 route.ts 不能在同一目录:
app/└── api/ └── users/ ├── page.tsx # ❌ 冲突 └── route.ts # ❌ 冲突下一篇将深入嵌套路由与布局,探索如何构建复杂的页面结构。
-EOF-