mcpField
N
nextjs-dynamic-routes-params

nextjs-dynamic-routes-params

github.com/wsimmonds开发109303 次安装53 周前更新
代码仓库

关于 nextjs-dynamic-routes-params

面向 Next.js 开发者的动态路由技能,介绍如何按 URL 路径段组织路由并读取参数。涵盖服务端与客户端参数访问、Next.js 15+ 的异步 params,以及 catch-all 路由。

主要功能

  1. 01创建动态路由段
  2. 02读取服务端 params
  3. 03使用 useParams() 读取客户端参数
  4. 04支持 catch-all 与可选 catch-all
  5. 05为 Next.js 15+ 等待 params

适用场景

  1. 01按 URL 标识符加载详情页
  2. 02构建带 slug 的文章页面
  3. 03实现嵌套文档路径
  4. 04避免动态路由结构过度嵌套

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-dynamic-routes-params

安装后在 Claude Code 中输入/nextjs-dynamic-routes-params确认已加载。

详细介绍

Next.js 动态路由和路径名参数

何时使用此技能

适用于以下场景:

  • 创建动态路由段(例如,blog/[slug]、users/[id])
  • 在服务器组件或客户端组件中访问 URL 路径名参数
  • 构建基于路由参数获取数据的页面
  • 实现全匹配或可选全匹配路由
  • 在 page.tsx、layout.tsx 或 route.ts 中使用 params 属性

⚠️ 识别何时需要动态路由

留意数据与 URL 路径相关联的需求。

只要 UI 依赖路径名的一部分,就应创建动态段([param])。常见信号包括:

  • 详情页会引用“URL 中的项目 ID/slug”
  • 文案中提到了路径段(例如,/products/{id}、/blog/{slug})
  • 需要“根据当前访问的资源”获取数据
  • 导航流程中,某个页面会链接到 /something/{identifier}

✅ 动态路由响应

Requirement: display product information based on whichever ID appears in the URL
Implementation: app/[id]/page.tsx
Access parameter with: const { id } = await params;

❌ 静态页面响应

Implementation: app/page.tsx  ← cannot access per-path identifiers

会引出动态路由的需求示例

  1. “显示一个产品页面,根据 URL 中的产品 ID 加载对应产品” → app/[id]/page.tsx 或 app/products/[id]/page.tsx
  2. “根据 slug 渲染博客文章” → app/blog/[slug]/page.tsx 或 app/[slug]/page.tsx
  3. “支持嵌套文档,例如 /docs/getting-started/installation” → app/docs/[...slug]/page.tsx

核心规则: 如果数据会随 URL 段变化,文件夹名称就必须使用相应的方括号。

⚠️ 重要:避免过度设计路由结构

最常见的错误: 为路由添加不必要的嵌套。

默认规则: 创建动态路由时,使用 app/[id]/page.tsx 或 app/[slug]/page.tsx,除非:

  • URL 结构有明确规定(例如,“在 /products/[id] 创建路由”)
  • 你正在构建多个需要使用命名空间的资源类型
  • 需求明确要求嵌套 URL 结构

不要根据资源名称推断嵌套结构:

  • “按 ID 获取产品” → app/[id]/page.tsx ✅(而不是 app/products/[id])
  • “显示用户个人资料” → app/[userId]/page.tsx ✅(而不是 app/users/[userId])
  • “显示博客文章” → app/[slug]/page.tsx ✅(而不是 app/blog/[slug])

仅在明确要求时嵌套:

  • “在 /blog/[slug] 创建路由” → app/blog/[slug]/page.tsx ✅
  • “产品页面应位于 /products/[id]” → app/products/[id]/page.tsx ✅

核心概念

动态路由语法

Next.js 使用带方括号的文件夹名称创建动态路由段:

app/
├── [id]/page.tsx              # Matches /123, /abc, etc.
├── blog/[slug]/page.tsx       # Matches /blog/hello-world
├── shop/[category]/[id]/page.tsx  # Matches /shop/electronics/123
└── docs/[...slug]/page.tsx    # Matches /docs/a, /docs/a/b, /docs/a/b/c

**核心原则:**文件夹结构就是路由结构。

路由结构决策树

关键规则:不要根据资源类型名称推断路由结构!

仅仅因为你要获取一个“产品”或“用户”,并不意味着你需要 /products/[id] 或 /users/[id]。除非有明确要求,否则优先选择最简单的结构。

决定路由结构时:

  1. 顶层动态路由(app/[id]/page.tsx)

    • 默认选择——除非有明确要求,否则使用此结构
    • 当资源本身就是主要实体时使用
    • 当只需要基于 ID 的路由时使用
    • 示例:任何资源都可以使用 /123,slug 可以使用 /abc-def
    • 模式:ID/slug 是唯一需要的标识符
    • 不确定时,就选这个!
  2. 嵌套动态路由(app/category/[id]/page.tsx)

    • 仅在 URL 结构明确要求时使用
    • 当收到“创建一个 /products/[id] 路由”这样的要求时使用
    • 当 URL 本身需要包含类别前缀时使用
    • 示例:/products/123、/blog/my-post(在明确指定时)
    • 模式:类别 + 标识符(两者都必需时)
  3. 多段动态路由(app/[cat]/[id]/page.tsx)

    • 当层级关系很重要时使用
    • 示例:/shop/electronics/123
    • 模式:多个分类层级

⚠️ 常见错误: 应该创建 app/[id]/page.tsx 时,却创建了 app/products/[id]/page.tsx

❌ 错误:“通过 ID 获取产品” → app/products/[id]/page.tsx ✅ 正确:“通过 ID 获取产品” → app/[id]/page.tsx

❌ 错误:“为用户创建动态路由” → app/users/[userId]/page.tsx ✅ 正确:“为用户创建动态路由” → app/[userId]/page.tsx

仅在以下情况下添加类别前缀:

  • 需求明确指出“位于 /products/...”或类似路径
  • 你正在构建多个需要命名空间的资源类型
  • 需求中指定了 URL 结构

访问路径名参数

在服务端组件中(page.tsx、layout.tsx)

关键:在 Next.js 15+ 中,params 是一个 Promise,必须使用 await!

// ✅ CORRECT - Next.js 15+
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>;
}
// ❌ WRONG - Treating params as synchronous object (Next.js 15+)
export default async function ProductPage({
  params,
}: {
  params: { id: string };  // Missing Promise wrapper
}) {
  const product = await fetch(`https://api.example.com/products/${params.id}`);
  // This will fail because params is a Promise!
}

对于 Next.js 14 及更早版本:

// Next.js 14 - params is synchronous
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>;
}
在路由处理程序中(route.ts)
// 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。 请改用以下方法:

  1. 使用 useParams() 钩子:
'use client';

import { useParams } from 'next/navigation';

export function ProductClient() {
  const params = useParams<{ id: string }>();
  const id = params.id;

  // Use the id...
}
  1. 从服务端组件传递 params:
// 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} />;
}

// components/ProductClient.tsx
'use client';

export function ProductClient({ productId }: { productId: string }) {
  // Use productId...
}

常见模式

模式 1:简单的基于 ID 的页面
// app/[id]/page.tsx - Top-level dynamic route
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>
  );
}
模式 2:带 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>
  );
}

// Generate static params for SSG
export async function generateStaticParams() {
  const posts = await getAllPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}
模式 3:嵌套资源
// 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>
  );
}
模式 4:全捕获路由
// app/docs/[...slug]/page.tsx - Matches /docs/a, /docs/a/b, /docs/a/b/c
interface PageProps {
  params: Promise<{
    slug: string[];  // Array of path segments
  }>;
}

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>;
}
// app/shop/[[...slug]]/page.tsx - Optional catch-all
// Matches /shop, /shop/electronics, /shop/electronics/phones
interface PageProps {
  params: Promise<{
    slug?: string[];  // Optional array
  }>;
}

export default async function ShopPage({ params }: PageProps) {
  const { slug = [] } = await params;

  if (slug.length === 0) {
    return <ShopHomepage />;
  }

  return <CategoryPage category={slug.join('/')} />;
}

TypeScript 最佳实践

params 的类型安全
// Define params type separately for reusability
type ProductPageParams = { id: string };

interface ProductPageProps {
  params: Promise<ProductPageParams>;
}

export default async function ProductPage({ params }: ProductPageProps) {
  const { id } = await params;
  // id is typed as string
}

// Reuse in generateMetadata
export async function generateMetadata({ params }: ProductPageProps) {
  const { id } = await params;
  const product = await getProduct(id);
  return { title: product.name };
}
多个动态片段
type PostPageParams = {
  category: string;
  slug: string;
};

interface PostPageProps {
  params: Promise<PostPageParams>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function PostPage({ params, searchParams }: PostPageProps) {
  const { category, slug } = await params;
  const { view } = await searchParams;

  // All properly typed
}

常见问题与解决方案

问题 1:忘记 params 是一个 Promise(Next.js 15+)
// ❌ WRONG
export default async function Page({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);  // Error: params is Promise
}

// ✅ CORRECT
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);
}
问题 2:在客户端组件中使用 params,却没有使用 useParams
// ❌ WRONG - params prop doesn't exist in Client Components
'use client';

export default function ClientPage({ params }) {  // undefined!
  return <div>{params.id}</div>;
}

// ✅ CORRECT
'use client';
import { useParams } from 'next/navigation';

export default function ClientPage() {
  const params = useParams<{ id: string }>();
  return <div>{params.id}</div>;
}
问题 3:路由嵌套过深
// ❌ UNNECESSARY NESTING
// app/products/product/[id]/page.tsx
// URL: /products/product/123

// ✅ SIMPLER
// app/products/[id]/page.tsx
// URL: /products/123

// OR even simpler if product is the main resource:
// app/[id]/page.tsx
// URL: /123
问题 4:未处理无效 ID
// ✅ ROBUST
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 => {
      if (!res.ok) throw new Error('Product not found');
      return res.json();
    });

  if (!product) {
    notFound();  // Shows 404 page
  }

  return <div>{product.name}</div>;
}

决策指南

需要创建动态路由时,请问自己:

  1. URL 结构是什么?

    • 单个 ID:[id]
    • 类别 + ID:category/[id]
    • 分层结构:[category]/[id]
    • 灵活路径:[...slug]
  2. 这是服务端组件还是客户端组件?

    • 服务端:使用 params prop(在 Next.js 15+ 中对其使用 await)
    • 客户端:使用 useParams() 钩子
  3. 是否需要最简单的结构?

    • 不确定时,减少嵌套层级
    • 顶层路由更简单、更直接
  4. 我使用的是 Next.js 15+ 吗?

    • 是:params 的类型是 Promise<{...}>
    • 否:params 的类型是 {...}

示例:实际场景

电商产品页面
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

async function getProduct(id: string): Promise<Product | null> {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 60 },  // Revalidate every 60s
  });

  if (!res.ok) return null;
  return res.json();
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);

  if (!product) {
    notFound();
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <p>{product.description}</p>
    </div>
  );
}

export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);

  return {
    title: product?.name ?? 'Product Not Found',
    description: product?.description,
  };
}
带嵌套路径的文档站点
// app/docs/[...slug]/page.tsx
interface DocPageProps {
  params: Promise<{ slug: string[] }>;
}

export default async function DocPage({ params }: DocPageProps) {
  const { slug } = await params;
  const path = slug.join('/');

  const doc = await getDocByPath(path);

  if (!doc) {
    notFound();
  }

  return (
    <article className="prose">
      <h1>{doc.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: doc.html }} />
    </article>
  );
}

export async function generateStaticParams() {
  const docs = await getAllDocs();

  return docs.map((doc) => ({
    slug: doc.path.split('/'),
  }));
}

动态路由检查清单

实现动态路由之前,请确认:

  • 路由结构尽可能简单(除非另有说明,否则默认使用顶层路由)
  • 不根据资源类型名称推断路由嵌套关系
  • 路由结构符合 URL 要求(避免过度嵌套)
  • 对于 Next.js 15+,params 的类型为 Promise<{...}>
  • 对于 Next.js 15+,访问属性前先等待 params
  • 处理无效 ID/slug 的错误(使用 notFound())
  • 正确定义 TypeScript 类型
  • 除非需要客户端交互,否则使用 Server Component
  • 如果适用,考虑使用 generateStaticParams() 进行 SSG
  • 如果 SEO 很重要,则生成元数据

快速参考

场景路由结构参数访问方式
按 ID 获取单个资源app/[id]/page.tsxconst { id } = await params
类别 + 资源app/category/[id]/page.tsxconst { id } = await params
带 slug 的博客app/blog/[slug]/page.tsxconst { slug } = await params
嵌套资源app/[cat]/[id]/page.tsxconst { cat, id } = await params
灵活路径app/docs/[...slug]/page.tsxconst { slug } = await params(slug 是数组)
可选路径app/[[...slug]]/page.tsxconst { slug = [] } = await params
Client Component使用 useParams() hookconst params = useParams<{ id: string }>()

请记住: Next.js 中的动态路由基于文件系统。使用 [brackets] 的文件夹结构会创建动态路由段,而 params 属性(或 useParams() hook)可用于访问这些值。

评论

支持 Markdown