Next.js 动态路由与参数处理完全攻略:从入门到类型安全
引言
上周在重构一个 Next.js 项目时,我遇到了个让人抓狂的问题——明明按照文档写的动态路由,点进去却是 404。看着控制台一片安静,没有任何报错信息,我整个人都懵了。后来才发现,Next.js 14 的 App Router 把路由参数的获取方式改了,我还在用 Pages Router 的老写法。
说实话,这不是我第一次在 Next.js 路由上栽跟头。从 Pages Router 的 getStaticPaths 到 App Router 的 generateStaticParams,每次升级都得重新学一遍。什么时候该用动态路由,什么时候该用 catch-all 路由,可选参数又是怎么回事?这些概念混在一起,真的容易搞糊涂。
如果你也跟我一样,对 Next.js 的动态路由感到困惑,或者正在从 Pages Router 迁移到 App Router,那这篇文章就是为你准备的。我会从最基础的动态路由讲起,一直讲到类型安全的实践技巧,用大量实际代码示例帮你理清思路。
学完后你能得到什么?一套完整的动态路由知识体系,知道各种场景该用什么路由类型,如何正确获取参数,以及如何用 TypeScript 让路由参数也有类型提示。不玩虚的,就是实打实的代码和解决方案。咱们开始吧。
第一章:动态路由基础(从最简单的开始)
什么是动态路由?
先说个最常见的场景:你有个博客网站,每篇文章的 URL 是 /blog/文章ID。如果用静态路由,你得为每篇文章创建一个单独的页面文件,这显然不现实。这时候就需要动态路由——一个页面文件处理所有文章详情。
在 Next.js App Router 中,动态路由通过方括号命名的文件夹来实现。听起来有点绕,直接看例子:
app/
├── blog/
│ └── [slug]/
│ └── page.tsx ← 这就是动态路由
这个结构会匹配所有 /blog/* 路径,比如:
/blog/hello-world→slug = "hello-world"/blog/nextjs-guide→slug = "nextjs-guide"/blog/123→slug = "123"
最简单的动态路由实现
创建 app/blog/[slug]/page.tsx,写下这段代码:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return (
<div>
<h1>文章详情</h1>
<p>当前文章 slug: {params.slug}</p>
</div>
)
}
就这么简单!当用户访问 /blog/hello-world 时,params.slug 就是 "hello-world"。
新手容易犯的错误:
- ❌ 文件名用
[slug].tsx(App Router 需要用文件夹) - ❌ 直接访问
props.slug(要通过params对象获取) - ❌ 忘记文件夹名的方括号(没方括号就是静态路由了)
Pages Router vs App Router 对比
如果你之前用过 Pages Router,可能会觉得奇怪:“以前不是在 pages/blog/[slug].tsx 里写吗?“是的,App Router 改了不少东西:
| 特性 | Pages Router | App Router |
|---|---|---|
| 文件位置 | pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
| 参数获取 | router.query.slug 或 getStaticProps | params.slug |
| 类型定义 | 需手动定义 | 通过 props 类型推导 |
| 静态生成 | getStaticPaths | generateStaticParams |
我刚开始迁移时,最不习惯的就是参数获取方式。Pages Router 可以用 useRouter hook,App Router 的 Server Components 不能用 hooks,只能通过 params prop。这是因为 Server Components 默认在服务端渲染,没有客户端的 router 对象。
实战案例:电商产品详情页
假设你在做个电商网站,产品详情页的 URL 是 /products/产品ID。完整实现是这样的:
// app/products/[id]/page.tsx
interface Product {
id: string
name: string
price: number
description: string
}
// 模拟从数据库获取产品
async function getProduct(id: string): Promise<Product | null> {
// 实际项目中这里是数据库查询或 API 调用
const products: Product[] = [
{ id: '1', name: 'TypeScript 入门书', price: 99, description: '适合初学者' },
{ id: '2', name: 'React 实战指南', price: 129, description: '从零到项目上线' }
]
return products.find(p => p.id === id) || null
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
return <div>产品不存在</div>
}
return (
<div>
<h1>{product.name}</h1>
<p className="price">¥{product.price}</p>
<p>{product.description}</p>
</div>
)
}
注意这几个细节:
- 组件用了
async,因为 Server Components 支持异步 - 先获取数据,再根据结果决定渲染什么
- 处理了产品不存在的情况(404 场景)
如果想返回真正的 404 页面,可以用 Next.js 的 notFound 函数:
import { notFound } from 'next/navigation'
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
notFound() // 返回 404 页面
}
return (
<div>
<h1>{product.name}</h1>
{/* ... */}
</div>
)
}
这样用户访问不存在的产品时,会看到你自定义的 not-found.tsx 页面,体验更好。
到这里,你已经掌握了基础的动态路由。但这只是冰山一角,接下来我们看看更复杂的场景——当你需要匹配多层路径时该怎么办。
第二章:Catch-All 路由与可选参数(处理复杂路径)
什么时候需要 Catch-All 路由?
假设你在做个文档网站,URL 结构是这样的:
/docs/getting-started/docs/api/authentication/docs/api/database/queries/docs/guides/deployment/vercel
路径层级不固定,可能是 2 层,也可能是 3 层或更多。用普通动态路由搞不定,这时候就需要 Catch-All 路由。
Catch-All 路由:[...slug]
文件夹命名用 [...slug](三个点),可以匹配任意层级的路径:
app/
├── docs/
│ └── [...slug]/
│ └── page.tsx ← 匹配 /docs/* 下所有路径
这会匹配:
/docs/getting-started→slug = ["getting-started"]/docs/api/authentication→slug = ["api", "authentication"]/docs/guides/deployment/vercel→slug = ["guides", "deployment", "vercel"]
注意:slug 参数是个数组,不是字符串!
代码实现:文档系统
// app/docs/[...slug]/page.tsx
interface Doc {
title: string
content: string
}
// 根据路径数组获取文档
async function getDoc(slugArray: string[]): Promise<Doc | null> {
// 把数组拼成路径,比如 ["api", "auth"] → "api/auth"
const path = slugArray.join('/')
// 实际项目中从文件系统或数据库读取
const docs: Record<string, Doc> = {
'getting-started': {
title: '快速开始',
content: '欢迎使用我们的产品...'
},
'api/authentication': {
title: 'API 认证',
content: '我们使用 JWT 进行认证...'
},
'api/database/queries': {
title: '数据库查询',
content: '使用 Prisma 查询数据库...'
}
}
return docs[path] || null
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
const doc = await getDoc(params.slug)
if (!doc) {
return <div>文档不存在</div>
}
return (
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.content }} />
{/* 面包屑导航 */}
<nav>
<a href="/docs">文档</a>
{params.slug.map((segment, i) => {
const href = `/docs/${params.slug.slice(0, i + 1).join('/')}`
return (
<span key={i}>
{' / '}
<a href={href}>{segment}</a>
</span>
)
})}
</nav>
</article>
)
}
这段代码的亮点:
- 用
slugArray.join('/')把路径数组拼成字符串 - 实现了面包屑导航,用
slice截取路径前缀 - 类型标注
params: { slug: string[] },TypeScript 会检查你没写错
可选 Catch-All 路由:[[...slug]]
有时候你希望既匹配 /docs,又匹配 /docs/*。普通 Catch-All 路由不会匹配 /docs(没有参数),这时候用 可选 Catch-All 路由:
app/
├── docs/
│ └── [[...slug]]/
│ └── page.tsx ← 注意是双层方括号
这会匹配:
/docs→slug = undefined/docs/getting-started→slug = ["getting-started"]/docs/api/auth→slug = ["api", "auth"]
代码里需要处理 slug 可能是 undefined 的情况:
// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
params
}: {
params: { slug?: string[] } // 注意 slug 是可选的
}) {
// 如果是 /docs 首页
if (!params.slug) {
return <div>欢迎来到文档中心</div>
}
// 处理子路径
const doc = await getDoc(params.slug)
// ...
}
新手容易踩的坑
坑1:忘记 slug 是数组
// ❌ 错误写法
<h1>当前路径: {params.slug}</h1> // 会显示 "api,authentication"
// ✅ 正确写法
<h1>当前路径: {params.slug.join('/')}</h1> // 显示 "api/authentication"
坑2:静态生成时数据结构不对
// ❌ 错误写法
export function generateStaticParams() {
return [
{ slug: 'api/auth' } // 这是字符串,不是数组!
]
}
// ✅ 正确写法
export function generateStaticParams() {
return [
{ slug: ['api', 'auth'] } // 数组形式
]
}
坑3:混淆普通动态路由和 Catch-All 路由
| 路由类型 | 文件夹命名 | 匹配范围 | 参数类型 |
|---|---|---|---|
| 动态路由 | [slug] | /blog/123 | string |
| Catch-All | [...slug] | /docs/a/b/c(不含 /docs) | string[] |
| 可选 Catch-All | [[...slug]] | /docs 和 /docs/a/b/c | string[] | undefined |
我当时就是把这三种混在一起用,结果路由一会儿能访问一会儿不能,查了半天才发现是文件夹命名写错了。
实战技巧:处理特殊字符
如果 URL 里有中文或特殊字符,记得做编解码:
export default async function Page({
params
}: {
params: { slug: string[] }
}) {
// URL 会自动编码,需要解码才能正常显示
const decodedSlug = params.slug.map(s => decodeURIComponent(s))
console.log(params.slug) // ["api", "%E8%AE%A4%E8%AF%81"]
console.log(decodedSlug) // ["api", "认证"]
// ...
}
到这里,你已经能处理各种复杂的路径结构了。但还有个关键问题没解决:这些动态页面什么时候生成?每次请求都渲染一遍,还是构建时提前生成好?这就是下一章要讲的 generateStaticParams。
第三章:generateStaticParams 深度解析(何时用、怎么用)
为什么需要 generateStaticParams?
假设你的博客有 100 篇文章,每篇文章都是动态路由 /blog/[slug]。如果不做优化,用户每次访问都要:
- 查询数据库获取文章内容
- 服务端渲染 HTML
- 返回给用户
这样响应慢,服务器压力大。Next.js 提供了更好的方案——在构建时预渲染所有文章页面,生成静态 HTML。这就是 generateStaticParams 的作用。
基础用法:静态生成博客文章
// app/blog/[slug]/page.tsx
interface Post {
slug: string
title: string
content: string
}
// 获取所有文章的 slug
export async function generateStaticParams() {
// 从数据库或 CMS 获取所有文章
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// 返回所有可能的参数组合
return posts.map((post: Post) => ({
slug: post.slug
}))
}
// 渲染文章详情
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
// 根据 slug 获取文章内容
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
这段代码做了什么?
generateStaticParams在构建时运行,返回所有文章的 slug- Next.js 为每个 slug 预渲染一个静态 HTML 文件
- 用户访问时直接返回静态文件,超级快
构建后的产物:
.next/server/app/blog/
├── hello-world.html
├── nextjs-guide.html
└── typescript-tips.html
何时用 generateStaticParams?
这是我被问得最多的问题。简单判断:
✅ 适合用 generateStaticParams 的场景:
- 博客文章、新闻详情(内容相对固定)
- 产品详情页(产品数量有限,比如 < 10000)
- 文档页面、帮助中心
- 用户个人主页(如果用户量不大)
❌ 不适合用的场景:
- 搜索结果页(参数组合无限)
- 实时数据(股票行情、体育比分)
- 用户量巨大的 UGC 平台(不可能预渲染所有用户页面)
- 需要根据登录状态显示不同内容的页面
进阶用法1:Catch-All 路由的静态生成
对于 [...slug] 路由,返回的参数要是数组:
// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
// 所有文档路径
const docPaths = [
['getting-started'],
['api', 'authentication'],
['api', 'database', 'queries'],
['guides', 'deployment', 'vercel']
]
return docPaths.map(slug => ({ slug }))
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
// ...
}
注意:返回的格式是 { slug: ['api', 'auth'] },不是 { slug: 'api/auth' }。
进阶用法2:多参数路由
如果路由有多个动态参数,比如 /shop/[category]/[productId]:
app/
├── shop/
│ └── [category]/
│ └── [productId]/
│ └── page.tsx
generateStaticParams 这样写:
// app/shop/[category]/[productId]/page.tsx
export async function generateStaticParams() {
const products = [
{ category: 'electronics', productId: 'iphone-15' },
{ category: 'electronics', productId: 'macbook-pro' },
{ category: 'books', productId: 'clean-code' },
{ category: 'books', productId: 'refactoring' }
]
return products.map(p => ({
category: p.category,
productId: p.productId
}))
}
export default async function ProductPage({
params
}: {
params: { category: string; productId: string }
}) {
return (
<div>
<h1>分类: {params.category}</h1>
<p>产品 ID: {params.productId}</p>
</div>
)
}
进阶用法3:按需生成(fallback 模式)
如果你的内容太多(比如 10 万篇文章),全部预渲染不现实。可以只生成热门内容,其余的按需生成:
// app/blog/[slug]/page.tsx
export const dynamicParams = true // 允许动态生成未预渲染的页面
export async function generateStaticParams() {
// 只预渲染前 100 篇热门文章
const topPosts = await fetchTopPosts(100)
return topPosts.map(post => ({
slug: post.slug
}))
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
// 即使没预渲染,第一次访问也会生成页面并缓存
const post = await fetchPost(params.slug)
if (!post) {
notFound()
}
return <article>{/* ... */}</article>
}
设置 dynamicParams = true 后:
- 预渲染的页面:立即返回(最快)
- 未预渲染的页面:第一次请求时生成,后续访问复用缓存
- 不存在的页面:返回 404
新手容易卡的地方
问题1:generateStaticParams 什么时候运行?
只在构建时(npm run build)运行,不是每次请求都运行。所以开发环境(npm run dev)看不到效果,必须构建后才能看到静态生成的文件。
问题2:数据更新了怎么办?
静态生成后,内容就固定了。如果数据更新,需要重新构建并部署。解决方案:
- 使用 ISR(Incremental Static Regeneration)定时更新
- 结合
dynamicParams = true按需更新 - 使用
revalidate设置缓存过期时间
// 每隔 60 秒重新生成页面
export const revalidate = 60
export default async function Page() {
// ...
}
问题3:为什么构建时间变长了?
generateStaticParams 返回的路径越多,构建时间越长。如果构建超时:
- 减少预渲染的页面数量(只渲染热门内容)
- 使用增量构建(Vercel/Netlify 支持)
- 考虑按需生成(
dynamicParams = true)
到这里,你已经掌握了 Next.js 动态路由的核心用法。最后一章,我们解决一个困扰很多人的问题——如何让路由参数也有 TypeScript 类型提示?
第四章:路由参数类型安全实践(告别 any)
为什么需要类型安全?
看这段代码,你能发现问题吗?
export default async function Page({
params
}: {
params: { slug: string }
}) {
// 假设这是个数字 ID,但类型定义是 string
const id = parseInt(params.slug)
if (isNaN(id)) {
// 运行时才发现类型不对!
return <div>无效的 ID</div>
}
// ...
}
问题是:params.slug 类型是 string,但你实际需要的是数字。这种类型不匹配,编译时发现不了,只能运行时报错。
基础类型约束
Next.js 的 params 对象默认所有参数都是 string 或 string[]。你可以自定义类型来增强约束:
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// TypeScript 知道 params.slug 是 string
const post = await fetchPost(params.slug)
// ...
}
这看起来没多大用,但当参数多了就很有价值:
// app/shop/[category]/[productId]/page.tsx
interface ShopParams {
category: 'electronics' | 'books' | 'clothing' // 限定为几个值
productId: string
}
export default async function ProductPage({
params
}: {
params: ShopParams
}) {
// TypeScript 会检查 category 是否是允许的值
if (params.category === 'toys') { // ❌ 编译错误!
// ...
}
}
运行时验证:结合 Zod
类型定义只能在编译时检查,运行时还是可能传入非法值。结合 Zod 做运行时验证更安全:
npm install zod
// app/products/[id]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
// 定义参数的 schema
const paramsSchema = z.object({
id: z.string().regex(/^\d+$/, '必须是数字 ID')
})
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
// 运行时验证
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound() // 非法参数直接返回 404
}
const { id } = result.data
const product = await fetchProduct(parseInt(id))
// ...
}
这样做的好处:
- 编译时有类型检查
- 运行时验证参数格式
- 非法请求直接返回 404,不会查询数据库
高级技巧:类型安全的 generateStaticParams
generateStaticParams 的返回值也可以加类型约束:
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export async function generateStaticParams(): Promise<BlogParams[]> {
const posts = await fetchAllPosts()
return posts.map(post => ({
slug: post.slug
// 如果你写成 slug: post.id(类型不对),TypeScript 会报错
}))
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// ...
}
实战案例:多语言博客路由
假设你在做个多语言博客,URL 是 /[locale]/blog/[slug],比如:
/zh/blog/hello-world/en/blog/hello-world
完整的类型安全实现:
// app/[locale]/blog/[slug]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
// 支持的语言列表
const locales = ['zh', 'en', 'ja'] as const
type Locale = typeof locales[number] // "zh" | "en" | "ja"
interface PageParams {
locale: Locale
slug: string
}
// 运行时验证 schema
const paramsSchema = z.object({
locale: z.enum(locales),
slug: z.string().min(1)
})
export async function generateStaticParams(): Promise<PageParams[]> {
const posts = await fetchAllPosts()
// 为每个语言生成对应的路径
return locales.flatMap(locale =>
posts.map(post => ({
locale,
slug: post.slug
}))
)
}
export default async function BlogPost({
params
}: {
params: PageParams
}) {
// 运行时验证
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound()
}
const { locale, slug } = result.data
// 获取对应语言的文章
const post = await fetchPost(slug, locale)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
这段代码的优点:
Locale类型限定为"zh" | "en" | "ja",写错会报错generateStaticParams返回类型是PageParams[],确保结构正确- 运行时用 Zod 验证,防止非法请求
- 整个流程从类型定义到运行时验证都是严格的
常见类型问题排查
问题1:params 类型是 Promise<...> 怎么办?
Next.js 15 之后,params 可能是异步的。需要这样写:
export default async function Page({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params // 先 await
// ...
}
或者用同步版本(如果你确定是 Next.js 14):
export default async function Page({
params
}: {
params: { slug: string }
}) {
// 直接用
}
问题2:类型提示不准确
如果 TypeScript 提示 params 是 any,检查:
tsconfig.json是否启用了严格模式- 是否正确导入了 Next.js 的类型
- 文件命名是否正确(必须是
page.tsx)
问题3:Zod 验证失败但想看详细错误
const result = paramsSchema.safeParse(params)
if (!result.success) {
console.error('参数验证失败:', result.error.format())
notFound()
}
类型安全检查清单
在你的项目里,检查这几点确保类型安全:
- 所有动态路由页面都定义了
params类型 -
generateStaticParams返回值类型与params一致 - 关键路由使用了运行时验证(Zod)
- 启用了 TypeScript 严格模式
- 复杂参数使用了联合类型或字面量类型
做到这些,你的路由系统就基本不会出类型相关的 bug 了。
结论
如果你跟着这篇文章学到这里,恭喜你!你现在已经掌握了 Next.js 动态路由的完整知识体系。让我们回顾一下你学到了什么:
✅ 基础动态路由:用 [slug] 匹配单层路径,理解 params 参数获取方式
✅ Catch-All 路由:用 [...slug] 处理多层路径,知道可选参数的用法
✅ generateStaticParams:理解何时用、怎么用,以及按需生成的策略
✅ 类型安全实践:从编译时类型约束到运行时验证的完整方案
更重要的是,你理解了 App Router 和 Pages Router 的区别,不会再混淆两者的用法。你也知道了什么时候该预渲染,什么时候该按需生成,能根据实际场景选择合适的方案。
接下来可以做什么?
立即实践(不要拖延):
- 在你的项目里创建一个动态路由,试试
params参数获取 - 如果有多层路径需求,尝试 Catch-All 路由
- 给你的路由加上 TypeScript 类型定义和 Zod 验证
进阶学习(深入掌握):
- 并行路由:在同一个页面加载多个路由(
@folder语法) - 拦截路由:在不离开当前页面的情况下显示另一个路由(
(.)folder语法) - 路由组:用
(folder)组织路由但不影响 URL 结构 - 中间件:在路由级别做权限控制和重定向
学习资源(官方最权威):
- Next.js 官方文档 - 路由基础
- Next.js 官方文档 - 动态路由
- Next.js 官方文档 - generateStaticParams
- TypeScript Deep Dive - 提升 TypeScript 技能
常见问题速查:
| 问题 | 检查项 | 解决方案 |
|---|---|---|
| 访问动态路由 404 | 文件夹命名、generateStaticParams | 确认方括号正确,检查静态生成配置 |
params 是 any | TypeScript 配置 | 启用严格模式,定义参数类型 |
| 构建时间太长 | generateStaticParams 返回数量 | 减少预渲染页面,使用按需生成 |
| 数据不更新 | 缓存策略 | 设置 revalidate 或 dynamicParams |
最后想说的话
Next.js 的路由系统从 Pages Router 到 App Router 经历了很大变化,我知道很多人(包括我自己)都经历过迁移的阵痛。但一旦掌握了 App Router 的思维方式,你会发现它其实更直观、更强大。
动态路由只是 Next.js 的一个方面,但它是整个应用的基础。把路由搞明白了,后面的数据获取、缓存策略、中间件等概念学起来会顺畅很多。
如果你在实践中遇到问题:
- 先查官方文档的”Troubleshooting”章节
- 在 Next.js GitHub 仓库搜索相关 Issue
- 到 Next.js Discord 社区提问(英文,但响应很快)
别害怕试错,我当时也是折腾了好几个项目才完全理解 App Router 的路由机制。现在你有了这篇文章作为参考,应该能少走很多弯路。
现在,打开你的编辑器,开始构建你的动态路由吧!🚀
Next.js 动态路由配置完整流程
从创建动态路由到类型安全实践的完整步骤
⏱️ 预计耗时: 2 小时
- 1
步骤1: 创建动态路由文件夹
根据需求选择路由类型:
• 单参数:app/posts/[id]/page.tsx
• 多参数:app/posts/[category]/[id]/page.tsx
• Catch-all:app/posts/[...slug]/page.tsx
• 可选catch-all:app/posts/[[...slug]]/page.tsx
文件夹命名规则:
• [id]:必需参数
• [...slug]:捕获所有路径段
• [[...slug]]:可选捕获所有路径段 - 2
步骤2: 获取路由参数
在page.tsx中获取参数:
• App Router使用params对象
• params是Promise,需要await
• 使用解构获取具体参数
示例:
export default async function Page({ params }) {
const { id } = await params
return <div>Post {id}</div>
}
注意:params必须await,否则会报错 - 3
步骤3: 配置类型安全
使用TypeScript定义类型:
• 定义params类型接口
• 使用Promise<{ params }>类型
• 使用generateStaticParams返回类型
示例:
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
} - 4
步骤4: 实现静态生成(可选)
使用generateStaticParams:
• 返回所有可能的参数组合
• 支持async函数获取数据
• 用于静态生成所有页面
示例:
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ id: post.id }))
}
注意:只用于静态生成,动态路由不需要 - 5
步骤5: 处理可选参数
可选catch-all路由:
• 使用[[...slug]]语法
• params.slug可能是undefined
• 需要检查参数是否存在
示例:
export default async function Page({ params }) {
const { slug } = await params
if (!slug) {
return <div>All posts</div>
}
return <div>Category: {slug.join('/')}</div>
} - 6
步骤6: 测试和验证
测试要点:
• 测试所有路由是否正常
• 验证参数获取是否正确
• 检查类型提示是否正常
• 测试静态生成是否成功
检查清单:
• 所有动态路由都能正常访问
• 参数类型定义正确
• generateStaticParams返回正确数据
• 404错误已处理
常见问题
动态路由参数怎么获取?
关键点:
• params是Promise,必须await
• 使用解构获取具体参数
• 类型需要定义
示例:
export default async function Page({ params }) {
const { id } = await params
return <div>{id}</div>
}
为什么动态路由返回404?
• 文件夹命名错误(应该是[id]不是{id})
• 路径不匹配(检查URL和文件夹结构)
• generateStaticParams返回的数据不完整
• 缺少page.tsx文件
解决方法:
• 检查文件夹命名是否正确
• 确认URL路径与文件夹结构匹配
• 检查generateStaticParams返回值
catch-all 路由和可选 catch-all 有什么区别?
• 必须匹配至少一个路径段
• /posts/[...slug] 匹配 /posts/a,但不匹配 /posts
可选catch-all路由[[...slug]]:
• 可以匹配0个或多个路径段
• /posts/[[...slug]] 匹配 /posts 和 /posts/a/b
使用场景:
• catch-all:需要至少一个参数
• 可选catch-all:参数可选
如何实现类型安全的动态路由?
1) 定义params类型接口
2) 使用Promise<{ params }>类型
3) 使用generateStaticParams返回类型
示例:
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
}
generateStaticParams 什么时候用?
适用场景:
• 知道所有可能的参数值
• 需要静态生成所有页面
• 提升性能和SEO
不适用场景:
• 参数值动态变化
• 参数值太多无法枚举
• 需要实时数据
注意:只用于静态生成,动态路由不需要
如何从 Pages Router 迁移动态路由?
• getStaticPaths → generateStaticParams
• context.params → params(需要await)
• 返回格式从{ paths, fallback }改为数组
迁移步骤:
1) 将getStaticPaths改为generateStaticParams
2) 修改参数获取方式(使用await params)
3) 更新类型定义
4) 测试所有路由
多参数动态路由怎么处理?
app/posts/[category]/[id]/page.tsx
获取参数:
export default async function Page({ params }) {
const { category, id } = await params
return <div>{category} - {id}</div>
}
generateStaticParams返回所有组合:
export async function generateStaticParams() {
return [
{ category: 'tech', id: '1' },
{ category: 'tech', id: '2' },
// ...
]
}
13 分钟阅读 · 发布于: 2025年12月25日 · 修改于: 2026年1月22日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南

评论
使用 GitHub 账号登录后即可评论