动态路由

2026年9月6日

文章详情、商品页这类「同一套 UI、不同 id」用动态段。文件夹名用方括号包起来即可。


单段:[id]

app/shop/[id]/page.tsx   →  /shop/123、/shop/abc
text

服务端组件用 params(新版本是 Promise,要 await):

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <p>商品 {id}</p>;
}
tsx

/shop/123 里的 123 就是 id。名字可以叫 slugpostId,只要和文件夹一致。


捕获剩余段:[...slug]

[...slug] 吃掉后面所有片段,得到字符串数组:

app/shop/[...slug]/page.tsx
text
URLslug
/shop/a["a"]
/shop/a/b/c["a", "b", "c"]
/shop不匹配,404

适合文档树、任意深度的分类路径。


可选捕获:[[...slug]]

多一对括号,根路径也可以命中:

app/shop/[[...slug]]/page.tsx
text
URLslug
/shopundefined 或空(视版本而定,先按「可能没有」处理)
/shop/a["a"]
/shop/a/b["a", "b"]

一个页面同时扛列表和任意深度详情时,用这个最省事。


客户端读参数

客户端组件用 useParams

"use client";

import { useParams } from "next/navigation";

export default function ShopLabel() {
  const params = useParams<{ id?: string; slug?: string | string[] }>();
  return <span>{JSON.stringify(params)}</span>;
}
tsx

单段是字符串,捕获段是数组。不要在服务端组件里调这个 hook。

查询串(?tab=info)不是动态路由,用 searchParamsuseSearchParams


和静态段一起用

静态文件夹优先。例如同时有 app/shop/new/page.tsxapp/shop/[id]/page.tsx,访问 /shop/new 会走 new,不会把 new 当成 id

生成静态路径时,配合 generateStaticParams

export async function generateStaticParams() {
  return [{ id: "1" }, { id: "2" }];
}
tsx

构建期会为这些 id 预渲染;没列出来的,按你的动态策略在请求时再渲染。

下一篇是平行路由:同一布局里并排挂多块页面。


参考文档