用途
本技能用于指导 Next.js App Router 中动态路由(dynamic routes)与路径名参数(pathname parameters)的使用。当 UI 依赖 URL 路径的一部分时,就需要创建动态段([param])。典型信号包括:详情页需要引用 URL 中的 ID/slug、文案中出现 /products/{id} 或 /blog/{slug} 这类路径片段、需要“根据访问的资源”获取数据、以及一个页面链接到 /something/{identifier} 的导航流程。
主要能力
- 创建动态路由段,例如 blog/[slug]、users/[id]。
- 在服务端组件(page.tsx、layout.tsx)与路由处理器(route.ts)中访问 params。
- 在客户端组件中通过 useParams() 钩子访问参数,或由服务端组件传入。
- 实现 catch-all 路由([...slug])与可选 catch-all 路由([[...slug]])。
- 基于路由参数获取数据,并配合 generateStaticParams() 实现 SSG、generateMetadata() 生成元数据。
- 处理无效 ID/slug,使用 notFound() 显示 404。
使用方式
动态路由语法基于文件夹名中的方括号:
app/
├── [id]/page.tsx # 匹配 /123、/abc 等
├── blog/[slug]/page.tsx # 匹配 /blog/hello-world
├── shop/[category]/[id]/page.tsx # 匹配 /shop/electronics/123
└── docs/[...slug]/page.tsx # 匹配 /docs/a、/docs/a/b、/docs/a/b/c
核心原则:文件夹结构即路由结构。
在服务端组件中(Next.js 15+,params 是 Promise,必须 await):
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(`https://api.example.com/products/${id}`)
.then(res => res.json());
return <div>{product.name}</div>;
}
Next.js 14 及更早版本中 params 是同步对象:
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then(res => res.json());
return <div>{product.name}</div>;
}
在路由处理器中:
// app/api/products/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const product = await db.products.findById(id);
return Response.json(product);
}
在客户端组件中不能直接访问 params,需使用 useParams():
'use client';
import { useParams } from 'next/navigation';
export function ProductClient() {
const params = useParams<{ id: string }>();
const id = params.id;
}
或由服务端组件传入:
// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <ProductClient productId={id} />;
}
路由结构决策
关键规则:不要从资源类型名称推断路由结构。获取“product”或“user”并不意味着需要 /products/[id] 或 /users/[id]。除非明确说明,优先使用最简单的结构。
- 顶层动态路由(app/[id]/page.tsx):默认选择,除非特别说明。
- 嵌套动态路由(app/category/[id]/page.tsx):仅在 URL 结构明确要求时使用。
- 多段动态(app/[cat]/[id]/page.tsx):当层级关系重要时使用。
常见错误:
❌ 错误:“Fetch a product by ID” → app/products/[id]/page.tsx
✅ 正确:“Fetch a product by ID” → app/[id]/page.tsx
❌ 错误:“Create a dynamic route for users” → app/users/[userId]/page.tsx
✅ 正确:“Create a dynamic route for users” → app/[userId]/page.tsx
仅当需求明确写出“at /products/...”之类、需要为多种资源类型命名空间、或需求中指定了 URL 结构时,才添加类别前缀。
常见模式
简单 ID 页面:
// app/[id]/page.tsx
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function ItemPage({ params }: PageProps) {
const { id } = await params;
const item = await fetch(`https://api.example.com/items/${id}`)
.then(res => res.json());
return (
<div>
<h1>{item.title}</h1>
<p>{item.description}</p>
</div>
);
}
带 slug 的博客文章:
// app/blog/[slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
嵌套资源:
// app/users/[userId]/posts/[postId]/page.tsx
interface PageProps {
params: Promise<{ userId: string; postId: string }>;
}
export default async function UserPost({ params }: PageProps) {
const { userId, postId } = await params;
const [user, post] = await Promise.all([
getUserById(userId),
getPostById(postId),
]);
return (
<div>
<h1>{post.title}</h1>
<p>By {user.name}</p>
<div>{post.content}</div>
</div>
);
}
Catch-all 路由:
// app/docs/[...slug]/page.tsx - 匹配 /docs/a、/docs/a/b、/docs/a/b/c
interface PageProps {
params: Promise<{ slug: string[] }>;
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params;
const path = slug.join('/');
const doc = await getDocByPath(path);
return <div>{doc.content}</div>;
}
可选 catch-all:
// app/shop/[[...slug]]/page.tsx - 匹配 /shop、/shop/electronics、/shop/electronics/phones
interface PageProps {
params: Promise<{ slug?: string[] }>;
}
export default async function ShopPage({ params }: PageProps) {
const { slug = [] } = await params;
if (slug.length === 0) {
return <ShopHomepage />;
}
return <CategoryPage category={slug.join('/')} />;
}
常见陷阱
- 忘记 params 是 Promise(Next.js 15+):必须写成 params: Promise<{ id: string }> 并 await。
- 在客户端组件中直接使用 params:应改用 useParams() 钩子。
- 过度嵌套路由:如 app/products/product/[id]/page.tsx 应简化为 app/products/[id]/page.tsx 或 app/[id]/page.tsx。
- 未处理无效 ID:应检查响应并在失败时调用 notFound()。
适用场景
- 电商产品详情页(app/products/[id]/page.tsx)。
- 带嵌套路径的文档站点(app/docs/[...slug]/page.tsx)。
- 博客文章页(app/blog/[slug]/page.tsx)。
- 需要基于 URL 参数获取数据的任意页面。
限制
本技能资料未提供关于收费、价格、平台支持范围或性能效果的信息,暂未提供。使用时应以 Next.js 官方文档为准,并注意 Next.js 15+ 与 14 及更早版本在 params 处理上的差异。