动态路由
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。名字可以叫 slug、postId,只要和文件夹一致。
捕获剩余段:[...slug]
[...slug] 吃掉后面所有片段,得到字符串数组:
app/shop/[...slug]/page.tsx
text
| URL | slug |
|---|---|
/shop/a | ["a"] |
/shop/a/b/c | ["a", "b", "c"] |
/shop | 不匹配,404 |
适合文档树、任意深度的分类路径。
可选捕获:[[...slug]]
多一对括号,根路径也可以命中:
app/shop/[[...slug]]/page.tsx
text
| URL | slug |
|---|---|
/shop | undefined 或空(视版本而定,先按「可能没有」处理) |
/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)不是动态路由,用 searchParams 或 useSearchParams。
和静态段一起用
静态文件夹优先。例如同时有 app/shop/new/page.tsx 和 app/shop/[id]/page.tsx,访问 /shop/new 会走 new,不会把 new 当成 id。
生成静态路径时,配合 generateStaticParams:
export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }];
}
tsx
构建期会为这些 id 预渲染;没列出来的,按你的动态策略在请求时再渲染。
下一篇是平行路由:同一布局里并排挂多块页面。