Skip to content

动态片段

¥Dynamic Segments

如果你事先不知道确切的路由段名称,并且想要使用动态数据创建路由,则可以使用在请求时填写或在构建时预渲染的动态段。

¥When you don't know the exact route segment names ahead of time and want to create routes from dynamic data, you can use Dynamic Segments that are filled in at request time or prerendered at build time.

惯例

¥Convention

可以通过将文件夹名称括在方括号中来创建动态段:[folderName]。例如,博客可以包含以下路由 app/blog/[slug]/page.js,其中 [slug] 是博客帖子的动态分段。

¥A Dynamic Segment can be created by wrapping a folder's name in square brackets: [folderName]. For example, a blog could include the following route app/blog/[slug]/page.js where [slug] is the Dynamic Segment for blog posts.

tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return <div>My Post: {slug}</div>
}

动态段作为 params 属性传递给 layoutpageroutegenerateMetadata 函数。

¥Dynamic Segments are passed as the params prop to layout, page, route, and generateMetadata functions.

路由示例 URLparams
app/blog/[slug]/page.js/blog/a{ slug: 'a' }
app/blog/[slug]/page.js/blog/b{ slug: 'b' }
app/blog/[slug]/page.js/blog/c{ slug: 'c' }

在客户端组件中

¥In Client Components

在客户端组件页面中,可以使用 use 钩子访问来自 props 的动态片段。

¥In a Client Component page, dynamic segments from props can be accessed using the use hook.

tsx
'use client'
import { use } from 'react'

export default function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = use(params)

  return (
    <div>
      <p>{slug}</p>
    </div>
  )
}

或者,客户端组件可以使用 useParams 钩子在客户端组件树中的任何位置访问 params

¥Alternatively Client Components can use the useParams hook to access the params anywhere in the Client Component tree.

捕获所有片段

¥Catch-all Segments

通过在括号 [...folderName] 内添加省略号,可以扩展动态段以捕获所有后续段。

¥Dynamic Segments can be extended to catch-all subsequent segments by adding an ellipsis inside the brackets [...folderName].

例如,app/shop/[...slug]/page.js 将匹配 /shop/clothes,但也会匹配 /shop/clothes/tops/shop/clothes/tops/t-shirts 等。

¥For example, app/shop/[...slug]/page.js will match /shop/clothes, but also /shop/clothes/tops, /shop/clothes/tops/t-shirts, and so on.

路由示例 URLparams
app/shop/[...slug]/page.js/shop/a{ slug: ['a'] }
app/shop/[...slug]/page.js/shop/a/b{ slug: ['a', 'b'] }
app/shop/[...slug]/page.js/shop/a/b/c{ slug: ['a', 'b', 'c'] }

可选的包罗万象的段

¥Optional Catch-all Segments

通过将参数包含在双方括号中,可以使包罗万象的段成为可选:[[...folderName]]

¥Catch-all Segments can be made optional by including the parameter in double square brackets: [[...folderName]].

例如,除了 /shop/clothes/shop/clothes/tops/shop/clothes/tops/t-shirts 之外,app/shop/[[...slug]]/page.js 还会匹配 /shop

¥For example, app/shop/[[...slug]]/page.js will also match /shop, in addition to /shop/clothes, /shop/clothes/tops, /shop/clothes/tops/t-shirts.

catch-all 和可选的 catch-all 段之间的区别在于,使用可选时,不带参数的路由也会被匹配(上例中的 /shop)。

¥The difference between catch-all and optional catch-all segments is that with optional, the route without the parameter is also matched (/shop in the example above).

路由示例 URLparams
app/shop/[[...slug]]/page.js/shop{ slug: undefined }
app/shop/[[...slug]]/page.js/shop/a{ slug: ['a'] }
app/shop/[[...slug]]/page.js/shop/a/b{ slug: ['a', 'b'] }
app/shop/[[...slug]]/page.js/shop/a/b/c{ slug: ['a', 'b', 'c'] }

TypeScript

使用 TypeScript 时,你可以根据配置的路由段为 params 添加类型 - 分别使用 PageProps<'/route'>LayoutProps<'/route'>RouteContext<'/route'> 来在 pagelayoutroute 中键入 params

¥When using TypeScript, you can add types for params depending on your configured route segment — use PageProps<'/route'>, LayoutProps<'/route'>, or RouteContext<'/route'> to type params in page, layout, and route respectively.

路由 params 值被定义为 stringstring[]undefined(用于可选的兜底段),因为它们的值在运行时才确定。用户可以在地址栏中输入任何 URL,这些宽泛的类型有助于确保你的应用代码能够处理所有这些可能的情况。

¥Route params values are typed as string, string[], or undefined (for optional catch-all segments), because their values aren't known until runtime. Users can enter any URL into the address bar, and these broad types help ensure that your application code handles all these possible cases.

路由params 类型定义
app/blog/[slug]/page.js{ slug: string }
app/shop/[...slug]/page.js{ slug: string[] }
app/shop/[[...slug]]/page.js{ slug?: string[] }
app/[categoryId]/[itemId]/page.js{ categoryId: string, itemId: string }

如果你正在处理一个路由,其中​​ params 只能有固定数量的有效值,例如具有已知语言代码集的 [locale] 参数,则可以使用运行时验证来处理用户可能输入的任何无效参数,并让应用的其余部分使用已知集合中的更窄类型。

¥If you're working on a route where params can only have a fixed number of valid values, such as a [locale] param with a known set of language codes, you can use runtime validation to handle any invalid params a user may enter, and let the rest of your application work with the narrower type from your known set.

tsx
import { notFound } from 'next/navigation'
import type { Locale } from '@i18n/types'
import { isValidLocale } from '@i18n/utils'

function assertValidLocale(value: string): asserts value is Locale {
  if (!isValidLocale(value)) notFound()
}

export default async function Page(props: PageProps<'/[locale]'>) {
  const { locale } = await props.params // locale is typed as string
  assertValidLocale(locale)
  // locale is now typed as Locale
}

行为

¥Behavior

  • 由于 params prop 是一个 promise。你必须使用 async/await 或 React 的 use 函数来访问值。

    ¥Since the params prop is a promise. You must use async/await or React's use function to access the values.

    • 在版本 14 及更早版本中,params 是一个同步 prop。为了帮助向后兼容,你仍然可以在 Next.js 15 中同步访问它,但此行为将来会被弃用。

      ¥In version 14 and earlier, params was a synchronous prop. To help with backwards compatibility, you can still access it synchronously in Next.js 15, but this behavior will be deprecated in the future.

使用缓存组件

¥With Cache Components

缓存组件 与动态路由段一起使用时,参数的处理方式取决于是否使用 generateStaticParams

¥When using Cache Components with dynamic route segments, how you handle params depends on whether you use generateStaticParams.

如果没有 generateStaticParams,预渲染期间参数值未知,因此参数是运行时数据。你必须将参数访问封装在 <Suspense> 边界中,以提供备用 UI。

¥Without generateStaticParams, param values are unknown during prerendering, making params runtime data. You must wrap param access in <Suspense> boundaries to provide fallback UI.

使用 generateStaticParams,你可以提供可在构建时使用的示例参数值。构建过程会验证动态内容和其他运行时 API 是否得到正确处理,然后为示例生成静态 HTML 文件。使用运行时参数渲染的页面会在首次请求成功后保存到磁盘。

¥With generateStaticParams, you provide sample param values that can be used at build time. The build process validates that dynamic content and other runtime APIs are correctly handled, then generates static HTML files for the samples. Pages rendered with runtime params are saved to disk after a successful first request.

以下部分演示了这两种模式。

¥The sections below demonstrate both patterns.

不使用 generateStaticParams

¥Without generateStaticParams

所有参数均为运行时数据。参数访问必须由 Suspense 回退 UI 封装。Next.js 会在构建时生成一个静态框架,内容会在每个请求时加载。

¥All params are runtime data. Param access must be wrapped by Suspense fallback UI. Next.js generates a static shell at build time, and content loads on each request.

需要了解:你还可以使用 loading.tsx 来实现页面级回退 UI。

¥Good to know: You can also use loading.tsx for page-level fallback UI.

tsx
import { Suspense } from 'react'

export default function Page({ params }: PageProps<'/blog/[slug]'>) {
  return (
    <div>
      <h1>Blog Post</h1>
      <Suspense fallback={<div>Loading...</div>}>
        {params.then(({ slug }) => (
          <Content slug={slug} />
        ))}
      </Suspense>
    </div>
  )
}

async function Content({ slug }: { slug: string }) {
  const res = await fetch(`https://api.vercel.app/blog/${slug}`)
  const post = await res.json()

  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
    </article>
  )
}

使用 generateStaticParams

¥With generateStaticParams

提前提供参数,以便在构建时预渲染页面。你可以根据需要预渲染所有路由或部分路由。

¥Provide params ahead of time to prerender pages at build time. You can prerender all routes or a subset depending on your needs.

在构建过程中,路由会使用每个示例参数执行,以收集 HTML 结果。如果动态内容或运行时数据的访问不正确,构建将会失败。

¥During the build process, the route is executed with each sample param to collect the HTML result. If dynamic content or runtime data are accessed incorrectly, the build will fail.

tsx
import { Suspense } from 'react'

export async function generateStaticParams() {
  return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
}

export default async function Page({ params }: PageProps<'/blog/[slug]'>) {
  const { slug } = await params

  return (
    <div>
      <h1>Blog Post</h1>
      <Content slug={slug} />
    </div>
  )
}

async function Content({ slug }: { slug: string }) {
  const post = await getPost(slug)
  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
    </article>
  )
}

async function getPost(slug: string) {
  'use cache'
  const res = await fetch(`https://api.vercel.app/blog/${slug}`)
  return res.json()
}

构建时验证仅涵盖使用示例参数执行的代码路径。如果你的路由包含条件逻辑,需要访问运行时 API 来获取示例中未包含的某些参数值,则这些分支在构建时将不会被验证:

¥Build-time validation only covers code paths that execute with the sample params. If your route has conditional logic that accesses runtime APIs for certain param values not in your samples, those branches won't be validated at build time:

tsx
import { cookies } from 'next/headers'

export async function generateStaticParams() {
  return [{ slug: 'public-post' }, { slug: 'hello-world' }]
}

export default async function Page({ params }: PageProps<'/blog/[slug]'>) {
  const { slug } = await params

  if (slug.startsWith('private-')) {
    // This branch is never executed at build time
    // Runtime requests for 'private-*' slugs will error
    return <PrivatePost slug={slug} />
  }

  return <PublicPost slug={slug} />
}

async function PrivatePost({ slug }: { slug: string }) {
  const token = (await cookies()).get('token')
  // ... fetch and render private post using token for auth
}

对于 generateStaticParams 未返回的运行时参数,验证会在首次请求期间进行。在上面的示例中,对以 private- 开头的 slug 的请求将失败,因为 PrivatePost 在没有 Suspense 边界的情况下访问了 cookies()。其他未触发条件分支的运行时参数将成功渲染并保存到磁盘以供后续请求使用。

¥For runtime params not returned by generateStaticParams, validation occurs during the first request. In the example above, requests for slugs starting with private- will fail because PrivatePost accesses cookies() without a Suspense boundary. Other runtime params that don't hit the conditional branch will render successfully and be saved to disk for subsequent requests.

要解决此问题,请使用 Suspense 封装 PrivatePost

¥To fix this, wrap PrivatePost with Suspense:

tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'

export async function generateStaticParams() {
  return [{ slug: 'public-post' }, { slug: 'hello-world' }]
}

export default async function Page({ params }: PageProps<'/blog/[slug]'>) {
  const { slug } = await params

  if (slug.startsWith('private-')) {
    return (
      <Suspense fallback={<div>Loading...</div>}>
        <PrivatePost slug={slug} />
      </Suspense>
    )
  }

  return <PublicPost slug={slug} />
}

async function PrivatePost({ slug }: { slug: string }) {
  const token = (await cookies()).get('token')
  // ... fetch and render private post using token for auth
}

示例

¥Examples

使用 generateStaticParams

¥With generateStaticParams

generateStaticParams 函数可用于在构建时对路由进行 静态生成 操作,而不是在请求时按需操作。

¥The generateStaticParams function can be used to statically generate routes at build time instead of on-demand at request time.

tsx
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())

  return posts.map((post) => ({
    slug: post.slug,
  }))
}

generateStaticParams 函数中使用 fetch 时,请求为 自动去重。这避免了对相同的数据布局、页面和其他 generateStaticParams 函数进行多次网络调用,从而加快了构建时间。

¥When using fetch inside the generateStaticParams function, the requests are automatically deduplicated. This avoids multiple network calls for the same data Layouts, Pages, and other generateStaticParams functions, speeding up build time.

使用 generateStaticParams 的动态 GET 路由处理程序

¥Dynamic GET Route Handlers with generateStaticParams

generateStaticParams 还与动态 路由处理程序 配合使用,在构建时静态生成 API 响应:

¥generateStaticParams also works with dynamic Route Handlers to statically generate API responses at build time:

ts
export async function generateStaticParams() {
  const posts: { id: number }[] = await fetch(
    'https://api.vercel.app/blog'
  ).then((res) => res.json())

  return posts.map((post) => ({
    id: `${post.id}`,
  }))
}

export async function GET(
  request: Request,
  { params }: RouteContext<'/api/posts/[id]'>
) {
  const { id } = await params
  const res = await fetch(`https://api.vercel.app/blog/${id}`)

  if (!res.ok) {
    return Response.json({ error: 'Post not found' }, { status: 404 })
  }

  const post = await res.json()
  return Response.json(post)
}

在本例中,generateStaticParams 返回的所有博客文章 ID 的路由处理程序将在构建时静态生成。对其他 ID 的请求将在请求时动态处理。

¥In this example, route handlers for all blog post IDs returned by generateStaticParams will be statically generated at build time. Requests to other IDs will be handled dynamically at request time.