SuperAPI AI 工具集

nextjs-dynamic-routes-params

编程与开发

查看项目

更新于 10-15 16:35

技能介绍

用途

本技能用于指导 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('/')} />;
}

常见陷阱

  1. 忘记 params 是 Promise(Next.js 15+):必须写成 params: Promise<{ id: string }> 并 await。
  2. 在客户端组件中直接使用 params:应改用 useParams() 钩子。
  3. 过度嵌套路由:如 app/products/product/[id]/page.tsx 应简化为 app/products/[id]/page.tsx 或 app/[id]/page.tsx。
  4. 未处理无效 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 处理上的差异。