{T}

常用函数与方法

本文汇总 Next.js App Router 中常用的函数与方法,包括:

请求相关:fetch、cookies、headers、NextRequest、NextResponse、redirect、permanentRedirect、notFound、useParams、usePathname、useRouter、useSearchParams

渲染与缓存:generateStaticParams、generateViewport、revalidatePath、revalidateTag、unstable_cache、unstable_noStore、useSelectedLayoutSegment、useSelectedLayoutSegments

1. fetch

1.1. 介绍

Next.js 扩展了原生的 Web fetch API,可以为每个请求设置自己的缓存模式,你可以在服务端组件中搭配 asyncawait 直接调用:

javascript
// app/page.js
export default async function Page() {
  // 请求会被缓存
  // 类似于 Pages Router 下的 `getStaticProps`.
  // `force-cache` 是默认选项,不写也行
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })
 
  // 每次请求的时候都会重新获取
  // 类似于 Pages Router 下的 `getServerSideProps`.
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
 
  // 请求会被缓存,最多缓存 10s
  // 类似于 Pages Router 下的 `getStaticProps` 使用 `revalidate` 选项.
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })
 
  return <div>...</div>
}

这里要注意的是,浏览器中的 fetch 其实也是有 cache 选项的:

javascript
async function postData(url = "", data = {}) {
  const response = await fetch(url, {
    method: "POST",
    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
    body: JSON.stringify(data), 
  });
  return response.json();
}

浏览器中的 fetch cache 选项控制的是与浏览器交互的 HTTP 缓存,而我们在服务端中用的 fetch cache 选项控制的其实是 Next.js 自己的缓存逻辑,它会将这些请求缓存起来,方便以后重复请求的时候用到。它们具体的 cache 选项内容也会有所不同,接下来会讲到。

1.2. fetch(url, options)

options.cache

用于配置 Next.js 数据缓存(Data Cache

javascript
fetch(`https://...`, { cache: 'force-cache' | 'no-store' })
  • force-cache是默认值,表示优先从缓存中查找匹配请求,当没有匹配项或者匹配项过时时,才从服务器上获取资源并更新缓存。
  • no-store表示每次请求都从服务器上获取资源,不从缓存中查,也不更新缓存。

如果没有提供 cache 选项,默认为 force-cache,但如果你使用了动态函数(如 cookies()),它的默认值就会是 no-store

options.next.revalidate

javascript
fetch(`https://...`, { next: { revalidate: false | 0 | number } })

设置资源的缓存时间:

  • false(默认):语义上相当于 revalidate: Infinity,资源无限期缓存
  • 0:防止资源被缓存
  • number :指定资源的缓存时间,最多 n

如果一个单独的 fetch() 请求的 revalidate 值比路由段配置中的 revalidate 还低,整个路由的 revalidate 时间都会减少。如果同一路由下有两个使用相同 URL 的请求,但设置了不同的 revalidate值,用较低的那个值。

为了方便,如果 revalidate 设置了数字,无须再设置 cache 选项,设置为0 会应用 cache: 'no-store',设置为正值会应用 cache: 'force-cache'。冲突的配置如 { revalidate: 0, cache: 'force-cache' }{ revalidate: 10, cache: 'no-store' }会导致报错。

options.next.tags

javascript
fetch(`https://...`, { next: { tags: ['collection'] } })

设置资源的缓存标签,数据可以使用 revalidateTag 按需重新验证。自定义标签的最大长度是 256 个字符。

2. cookies

2.1. 介绍

cookies 函数用于:

  1. 在服务端组件读取传入请求的 cookie
  2. 在 Server Action 或路由处理程序中写入返回请求的 cookie

注意:之前的文章里也多次提到,cookies() 是一个动态函数,因为其返回值无法提前知道。所以在页面或者布局中使用该函数会导致路由转变为动态渲染。

2.2. cookies

(await cookies()).get(name)

该方法传入一个 cookie 名,返回一个具有 namevalue 属性的对象。如果没有找到,返回 undefined,如果匹配到多个 cookie,则返回第一个匹配到的。

javascript
// app/page.js
import { cookies } from 'next/headers'
 
export default function Page() {
  const cookieStore = await cookies()
  // 如果匹配到,theme 的值为 { name: 'theme', value: 'xxxx' }
  // 如果没有匹配到,theme 的值为 undefined
  const theme = cookieStore.get('theme')
  return '...'
}

(await cookies()).getAll(name)

该方法类似于 get,但会以数组形式返回所有匹配到的 cookies ,匹配不到则返回空数组。如果没有指定 name,则返回所有可用的 cookie。

javascript
// app/page.js
import { cookies } from 'next/headers'
 
export default function Page() {
  const cookieStore = await cookies()
  // 如果匹配到,theme 的值为 [{ name: 'theme', value: 'xxxx' }]
  // 如果没有匹配到,theme 的值为 []
  const theme = cookieStore.get('theme')
  return '...'
}

另一个示例如下:

javascript
// app/page.js
import { cookies } from 'next/headers'
 
export default function Page() {
  const cookieStore = await cookies()
  return cookieStore.getAll().map((cookie) => (
    <div key={cookie.name}>
      <p>Name: {cookie.name}</p>
      <p>Value: {cookie.value}</p>
    </div>
  ))
}

cookies().has(name)

该方法传入一个 cookie 名,返回一个判断该 cookie 是否存在的布尔值。

javascript
// app/page.js
import { cookies } from 'next/headers'
 
export default function Page() {
  const cookiesList = cookies()
  // true | false
  const hasCookie = cookiesList.has('theme')
  return '...'
}

(await cookies()).set(name, value, options)

该方法用于设置 cookie。

javascript
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
async function create(data) {
  (await cookies()).set('name', 'lee')
  // or
  (await cookies()).set('name', 'lee', { secure: true })
  // or
  (await cookies()).set({
    name: 'name',
    value: 'lee',
    httpOnly: true,
    path: '/',
  })
}

具体 options 除了 name、value 通过查看源码可以得知,还有 domain、expires、httponly、maxage、path、samesite、secure、priority。

删除 cookie 的方式有多种:

(await cookies()).delete(name)

删除指定名称的 cookie

javascript
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function create(data) {
  (await cookies()).delete('name')
}
(await cookies()).set(name, '')

将指定名称的 cookie 设置为空值

javascript
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function create(data) {
  (await cookies()).set('name', '')
}
(await cookies()).set(name, value, { maxAge: 0 })

设置 maxAge 为 0,立即使 cookie 过期

javascript
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function create(data) {
  (await cookies()).set('name', 'value', { maxAge: 0 })
}
(await cookies()).set(name, value, { expires: timestamp })

设置 expires 为过去的值都会使 cookie 过期

javascript
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function create(data) {
  const oneDay = 24 * 60 * 60 * 1000
  (await cookies()).set('name', 'value', { expires: Date.now() - oneDay })
}

测试删除效果

如果你想要测试这些删除效果:

javascript
'use client'
// app/page.js
import { create } from './action'

export default async function Page({ params }) {
  const resolvedParams = await params
 
  return (
    <form action={create}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

效果如下:

3. headers

3.1. 介绍

headers() 函数用于从服务端组件中读取传入的 HTTP 请求头。它拓展了 Web Headers API。它是只读的,这意味着你不能 set/delete 返回的请求头。headers() 和 cookies() 一样都是动态函数,其返回值无法提前知道,一旦使用会导致路由切换到动态渲染。

javascript
// app/page.js
import { headers } from 'next/headers'
 
export default function Page() {
  const headersList = await headers()
  const referer = headersList.get('referer')
 
  return <div>Referer: {referer}</div>
}

3.2. API

javascript
const headersList = await headers()

headers() 不接收任何参数,返回一个只读的 Web Headers 对象,所以没有 set、append、delete 这些方法:

举个例子:

javascript
// app/page.js
import { headers } from 'next/headers'
 
async function getUser() {
  const headersInstance = headers()
  const authorization = headersInstance.get('authorization')
  // 转发 authorization header
  const res = await fetch('...', {
    headers: { authorization },
  })
  return res.json()
}
 
export default async function UserPage() {
  const user = await getUser()
  return <h1>{user.name}</h1>
}

4. NextRequest

4.1. 介绍

NextRequest 拓展了 Web Resquest API,提供了一些便捷的方法。

4.2. cookies

用于读取和更改请求的 Set-Cookie标头。

set(name, value)

设置 cookie:

javascript
// 请求会有一个 `Set-Cookie:show-banner=false;path=/home` 标头
request.cookies.set('show-banner', 'false')

get(name)

返回指定名称的 cookie 值,找不到就返回 undefined,多个就返回第一个:

javascript
// { name: 'show-banner', value: 'false', Path: '/home' }
request.cookies.get('show-banner')

getAll()

返回指定名称的 cookie 值,未指定则返回所有,数组形式:

javascript
// [
//   { name: 'experiments', value: 'new-pricing-page', Path: '/home' },
//   { name: 'experiments', value: 'winter-launch', Path: '/home' },
// ]
request.cookies.getAll('experiments')
// 返回所有 cookie 值
request.cookies.getAll()

delete(name)

用于删除 cookie:

javascript
// 返回 true 表示删除成功, false 表示没有删掉任何东西
request.cookies.delete('experiments')

has(name)

判断是否有该 cookie 值,有则返回 true,无则返回 false

javascript
request.cookies.has('experiments')

clear()

删除请求的 Set-Cookie 标头

javascript
request.cookies.clear()

4.3. nextUrl

拓展了原生的 URL API,提供了一些便捷的方法:

javascript
// 假设请求是 /home, pathname 是 /home
request.nextUrl.pathname
// 请求是 /home?name=lee, searchParams 是 { 'name': 'lee' }
request.nextUrl.searchParams

5. NextResponse

5.1. 介绍

NextResponse 拓展了 Web Response API,提供了一些便捷的方法。

5.2. cookies

用于读取和更改响应的 Set-Cookie 标头。

set(name, value)

javascript
// 请求未 /home
let response = NextResponse.next()
// 设置 cookie
response.cookies.set('show-banner', 'false')
// Response 的 Set-Cookie 标头为 `Set-Cookie:show-banner=false;path=/home`
return response

get(name)

javascript
// 假设请求为 /home
let response = NextResponse.next()
// { name: 'show-banner', value: 'false', Path: '/home' }
response.cookies.get('show-banner')

getAll()

javascript
// 假设请求为 /home
let response = NextResponse.next()
// [
//   { name: 'experiments', value: 'new-pricing-page', Path: '/home' },
//   { name: 'experiments', value: 'winter-launch', Path: '/home' },
// ]
response.cookies.getAll('experiments')
// 返回所有 cookie 值
response.cookies.getAll()

delete(name)

javascript
// 假设请求为 /home
let response = NextResponse.next()
// 返回 true 表示删除成功, false 表示没有删掉任何东西
response.cookies.delete('experiments')

5.3. json

使用给定的 JSON 正文生成响应:

javascript
// app/api/route.js
import { NextResponse } from 'next/server'
 
export async function GET(request) {
  return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}

5.4. redirect()

生成重定向到新 URL 的响应:

javascript
import { NextResponse } from 'next/server'
 
return NextResponse.redirect(new URL('/new', request.url))

NextResponse.redirect()方法使用前可以创建和更改 URL,举个例子,你可以使用 request.nextUrl 获取当前的 URL,然后据此更改成重定向的 URL:

javascript
import { NextResponse } from 'next/server'
 
const loginUrl = new URL('/login', request.url)
// 添加 ?from=/incoming-url 参数到 /login URL
loginUrl.searchParams.set('from', request.nextUrl.pathname)
// 重定向到新 URL
return NextResponse.redirect(loginUrl)

5.5. rewrite()

保留原始 URL 的同时生成一个重写到指定 URL 的响应:

javascript
import { NextResponse } from 'next/server'
 
// 传入请求: /about, 浏览器显示 /about
// 重写请求: /proxy, 浏览器显示 /about
return NextResponse.rewrite(new URL('/proxy', request.url))

5.6. next()

常用在中间件,用于提前返回并继续路由:

javascript
import { NextResponse } from 'next/server'
 
return NextResponse.next()

也可以在生成响应的时候转发 headers

javascript
import { NextResponse } from 'next/server'
 
const newHeaders = new Headers(request.headers)
// 添加新 header
newHeaders.set('x-version', '123')
// 返回新的 headers
return NextResponse.next({
  request: {
    headers: newHeaders,
  },
})

6. redirect

6.1. 介绍

redirect函数,顾名思义,重定向地址,可用于服务端组件、路由处理程序、Server Actions。在 Streaming 中,使用重定向将插入一个 meta 标签以在客户端发起重定向,其他情况,它会返回一个 307 HTTP 重定向响应。如果资源不存在,可以直接使用 notFound 函数,并不一定需要 redirect 来处理。

redirect 函数接受两个参数:

javascript
redirect(path, type)

其中:

  • path 字符串类型,表示重定向的 URL,可以是相对路径,也可以是绝对路径
  • type 值为 replace (默认)或者 push(Server Actions 中默认),表示重定向的类型

默认情况下,redirect 在 Sever Actions 中会用 push(添加到浏览器历史栈),在其他地方用 replace(在浏览器历史栈中替换当前的 URL)。你可以通过指定 type参数覆盖此行为。

注意:在服务端组件中使用 type参数没有效果。

redirect 函数不返回任何值

举个例子:

javascript
// app/team/[id]/page.js
import { redirect } from 'next/navigation'
 
async function fetchTeam(id) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }) {
  const { id } = await params
  const team = await fetchTeam(id)
  if (!team) {
    redirect('/login')
  }
 
  // ...
}

7. permanentRedirect

7.1. 介绍

permanentRedirect,作用也是重定向,可用于服务端组件、客户端组件、路由处理程序、Server Actions。在 Streaming 中,使用重定向将插入一个 meta 标签以在客户端发起重定向,其他情况,它会返回一个 308 HTTP 重定向响应。。如果资源不存在,可以直接使用 notFound 函数。

permanentRedirect 函数接受两个参数:

javascript
permanentRedirect(path, type)

其中:

  • path 字符串类型,表示重定向的 URL,可以是相对路径,也可以是绝对路径
  • type 值为 replace (默认)或者 push(Server Actions 中默认),表示重定向的类型

默认情况下,permanentRedirect 在 Sever Actions 中会用 push(添加到浏览器历史栈),在其他地方用 replace(在浏览器历史栈中替换当前的 URL)。你可以通过指定 type参数覆盖此行为。

注意:在服务端组件中使用 type参数没有效果。

permanentRedirect 函数不返回任何值

举个例子:

javascript
// app/team/[id]/page.js
import { permanentRedirect } from 'next/navigation'
 
async function fetchTeam(id) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }) {
  const { id } = await params
  const team = await fetchTeam(id)
  if (!team) {
    permanentRedirect('/login')
  }
 
  // ...
}

8. notFound

8.1. 介绍

调用 notFound()函数会抛出一个 NEXT_NOT_FOUND错误,并且中止该路由段的渲染。通过声明一个 not-found.js文件可以为此路由段渲染一个 Not Found UI 来优雅的处理这个错误。

javascript
// app/user/[id]/page.js 
import { notFound } from 'next/navigation'
 
async function fetchUser(id) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }) {
  const { id } = await params
  const user = await fetchUser(id)
 
  if (!user) {
    notFound()
  }
 
  // ...
}

9. useParams

9.1. 介绍

useParams是一个客户端组件 hook,用于读取当前 URL 的动态参数:

javascript
'use client'
// app/example-client-component.js
import { useParams } from 'next/navigation'
 
export default function ExampleClientComponent() {
  const params = useParams()
 
  // 路由 -> /shop/[tag]/[item]
  // URL -> /shop/shoes/nike-air-max-97
  // `params` -> { tag: 'shoes', item: 'nike-air-max-97' }
  console.log(params)
 
  return <></>
}

9.2. 参数

useParams不接收任何参数。

javascript
const params = useParams()

9.3. 返回值

useParams 返回一个包含当前路由动态参数的对象,让我们直接看个例子就明白了:

Route 路线URL 网址useParams()
app/shop/page.js/shopnull
app/shop/[slug]/page.js/shop/1{ slug: '1' }
app/shop/[tag]/[item]/page.js/shop/1/2{ tag: '1', item: '2' }
app/shop/[...slug]/page.js/shop/1/2{ slug: ['1', '2'] }

10. usePathname

10.1. 介绍

usePathname 是一个客户端组件 hook,用于读取当前 URL 的 pathname。

javascript
'use client'
// app/example-client-component.js
import { usePathname } from 'next/navigation'
 
export default function ExampleClientComponent() {
  const pathname = usePathname()
  return <p>Current pathname: {pathname}</p>
}

usePathname 需要用在客户端组件中。

10.2. 参数

usePathname不接收任何参数。

javascript
const pathname = usePathname()

10.3. 返回值

usePathname 返回当前 URL pathname 的字符串,让我们直接看个例子就明白了:

URL返回值
/'/'
/dashboard'/dashboard'
/dashboard?v=2'/dashboard'
/blog/hello-world'/blog/hello-world'

举个例子:

javascript
'use client'
// app/example-client-component.js
import { usePathname, useSearchParams } from 'next/navigation'
 
function ExampleClientComponent() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
  useEffect(() => {
    // 监听路由变化
  }, [pathname, searchParams])
}

11. useRouter

11.1. 介绍

useRouter hook 用于在客户端组件中更改路由:

javascript
'use client'
// app/example-client-component.js
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

在 Next.js 中,优先推荐使用 <Link> 组件来导航,其次再针对一些特殊的需求使用 useRouter

11.2. useRouter()

push

router.push(href: string, { scroll: boolean })执行一个客户端导航,会将新地址添加到浏览器历史栈中

replace

router.replace(href: string, { scroll: boolean })执行一个客户端导航,但不会在浏览器历史栈中添加新的条目。

refresh

router.refresh() 刷新当前路由

prefetch

router.prefetch(href: string)预获取提供的路由,加快客户端导航速度

back

router.back() 向后导航到浏览器历史栈中的上一页

forward()

router.forward()向前导航到浏览器历史栈中的下一页

11.3. 示例

让我们看个例子:

javascript
'use client'
// app/components/navigation-events.js
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
 
export function NavigationEvents() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
 
  useEffect(() => {
    const url = `${pathname}?${searchParams}`
    console.log(url)
    // ...
  }, [pathname, searchParams])
 
  return null
}

注意:当使用 App Router 的时候,从next/navigation中导入 useRouter ,而非 next/router。Pages Router 下的 pathname 改为使用 usePathname(),Pages Router 下的 query 改为使用 useSearchParams()

在这个例子中,我们通过组合 usePathnameuseSearchParams 来监听页面更改。我们可以将这个函数导入到布局中:

javascript
// app/layout.js
import { Suspense } from 'react'
import { NavigationEvents } from './components/navigation-events'
 
export default function Layout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
 
        <Suspense fallback={null}>
          <NavigationEvents />
        </Suspense>
      </body>
    </html>
  )
}

在这个例子中,之所以能够生效,是因为在静态渲染的时候, useSearchParams()会导致客户端渲染到最近的 Suspense 边界。

再换一个例子,当导航到新路由时,Next.js 会默认滚动到页面的顶部。你可以在 router.push()router.replace()中传递 scroll: false来禁用该行为。

javascript
'use client'
// app/example-client-component.jsx
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button
      type="button"
      onClick={() => router.push('/dashboard', { scroll: false })}
    >
      Dashboard
    </button>
  )
}

12. useSearchParams

12.1. 介绍

useSearchParams是一个客户端组件 hook,用于读取当前 URL 的查询字符串。useSearchParams 返回一个只读版本的 URLSearchParams,举个例子:

javascript
'use client'
// app/dashboard/search-bar.js
import { useSearchParams } from 'next/navigation'
 
export default function SearchBar() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')
 
  // URL -> `/dashboard?search=my-project`
  // `search` -> 'my-project'
  return <>Search: {search}</>
}

12.2. 参数

useSearchParams 不接收任何参数。

javascript
const searchParams = useSearchParams()

12.3. 返回值

useSearchParams 返回一个只读版本的 URLSearchParams,它包含一些读取 URL 查询参数的工具方法,比如:

URLsearchParams.get("a")
/dashboard?a=1'1'
/dashboard?a=''
/dashboard?b=3null
/dashboard?a=1&a=2'1' (返回第一个,要获取所有,使用 getAll())
URLsearchParams.has("a")
/dashboard?a=1true
/dashboard?b=3false

其他方法还有 getAll()keys()values()entries()forEach()toString(),都是基于 URLSearchParams

12.4. 行为

静态渲染

如果路由是静态渲染,调用 useSearchParams() 会导致树到最近的 Suspense边界发生客户端渲染。应该尽可能将使用 useSearchParams 的组件放在 Suspense 边界中以减少客户端渲染的内容,举个例子:

javascript
'use client'
// app/dashboard/search-bar.js
import { useSearchParams } from 'next/navigation'
 
export default function SearchBar() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')

  // 当使用静态渲染的时候,不会在服务端打印
  console.log(search)
 
  return <>Search: {search}</>
}
javascript
// app/dashboard/page.js
import { Suspense } from 'react'
import SearchBar from './search-bar'
 
function SearchBarFallback() {
  return <>placeholder</>
}
 
export default function Page() {
  return (
    <>
      <nav>
        <Suspense fallback={<SearchBarFallback />}>
          <SearchBar />
        </Suspense>
      </nav>
      <h1>Dashboard</h1>
    </>
  )
}

动态渲染

如果路由是动态渲染的,在客户端组件的初始服务端渲染的时候,useSearchParams 在服务端是可用的。

javascript
'use client'
// app/dashboard/search-bar.js
import { useSearchParams } from 'next/navigation'
 
export default function SearchBar() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')

  // 初始渲染的时候会在服务端打印,后续导航中客户端也会打印
  console.log(search)
 
  return <>Search: {search}</>
}
javascript
// app/dashboard/page.js
import SearchBar from './search-bar'
 
export const dynamic = 'force-dynamic'
 
export default function Page() {
  return (
    <>
      <nav>
        <SearchBar />
      </nav>
      <h1>Dashboard</h1>
    </>
  )
}

服务端组件

在 Page(服务端组件)中获取参数,使用 searchParams prop。 Layout 中(服务端组件)并不会有 searchParams prop,这是因为在共享一个布局的多个页面之间导航的时候并不会重新渲染,这也就导致 searchParams 不会发生变化。所以要想获得准确的查询参数,使用 Page 的 searchParams prop 或是在客户端组件中使用 useSearchParams hook 它们会在客户端重新渲染的时候带上最新的 searchParams。

12.5. 示例

你可以使用 useRouter 或者 Link 设置新的 searchParams。当路由变化后,当前的 page.js 会收到一个更新的 searchParams prop:

javascript
// app/example-client-component.js
export default function ExampleClientComponent() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()
 
  const createQueryString = useCallback(
    (name, value) => {
      const params = new URLSearchParams(searchParams)
      params.set(name, value)
 
      return params.toString()
    },
    [searchParams]
  )
 
  return (
    <>
      <p>Sort By</p>
 
      {/* 使用 useRouter */}
      <button
        onClick={() => {
          // <pathname>?sort=asc
          router.push(pathname + '?' + createQueryString('sort', 'asc'))
        }}
      >
        ASC
      </button>
 
      {/* 使用 <Link> */}
      <Link
        href={
          // <pathname>?sort=desc
          pathname + '?' + createQueryString('sort', 'desc')
        }
      >
        DESC
      </Link>
    </>
  )
}

参考链接

  1. https://nextjs.org/docs/app/api-reference/functions

1. generateStaticParams

1.1. 介绍

generateStaticParams和动态路由一起使用,用于在构建时静态生成路由:

javascript
// app/product/[id]/page.js
export function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }, { id: '3' }]
}
 
// 对应会生成 3 个静态路由:
// - /product/1
// - /product/2
// - /product/3
export default async function Page({ params }) {
  const { id } = await params
  // ...
}

可以在 generateStaticParams 使用 fetch 请求,这个例子更贴近实际的开发场景:

javascript
// app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}
 
export default async function Page({ params }) {
  const { slug } = await params
  // ...
}

关于 generateStaticParams

  • 你可以使用 dynamicParams 路由段配置控制当访问不是由 generateStaticParams 生成的动态段时发生的情况
  • next dev的时候,当你导航到路由时,generateStaticParams才会被调用
  • next build的时候,generateStaticParams 会在对应的布局或页面生成之前运行
  • 在 重新验证(ISR)的时候,generateStaticParams 不会再次被调用
  • generateStaticParams 替代了 Pages Router 下的 getStaticPaths 函数的功能

上面这个例子是处理单个动态段,generateStaticParams 也可以处理多个动态段:

javascript
// app/products/[category]/[product]/page.js
export function generateStaticParams() {
  return [
    { category: 'a', product: '1' },
    { category: 'b', product: '2' },
    { category: 'c', product: '3' },
  ]
}
 
// 对应会生成 3 个静态路由:
// - /products/a/1
// - /products/b/2
// - /products/c/3
export default async function Page({ params }) {
  const { category, product } = await params
  // ...
}

也可以处理 Catch-all 动态段:

javascript
// app/product/[...slug]/page.js
export function generateStaticParams() {
  return [{ slug: ['a', '1'] }, { slug: ['b', '2'] }, { slug: ['c', '3'] }]
}
 
// 对应会生成 3 个静态路由:
// - /product/a/1
// - /product/b/2
// - /product/c/3
export default async function Page({ params }) {
  const { slug } = await params
  // ...
}

1.2. 参数

generateStaticParams 支持传入一个可选 options.params 参数。如果一个路由中的多个动态段都使用了 generateStaticParams,子 generateStaticParams 函数会为每一个父 generateStaticParams生成的 params 执行一次。

这句话是什么意思呢?举个例子,现在我们有这样一个 /products/[category]/[product]路由地址,这个路由里有两个动态段 [category][product][product] 依赖于 [category],毕竟要先知道类目才能该类目下知道有哪些产品。为了解决这个问题:

首先生成父段:

javascript
// app/products/[category]/layout.js
export async function generateStaticParams() {
  const products = await fetch('https://.../products').then((res) => res.json())
 
  return products.map((product) => ({
    category: product.category.slug,
  }))
}
 
export default async function Layout({ params }) {
  const resolvedParams = await params
  // ...
}

然后子 generateStaticParams函数就可以使用父 generateStaticParams函数返回的 params 参数动态生成自己的段:

javascript
// app/products/[category]/[product]/page.js
export async function generateStaticParams({ params: { category } }) {
  const products = await fetch(
    `https://.../products?category=${category}`
  ).then((res) => res.json())
 
  return products.map((product) => ({
    product: product.id,
  }))
}
 
export default async function Page({ params }) {
  const resolvedParams = await params
  // ...
}

在这个例子中,params 对象就包含了从父 generateStaticParams生成的 params,可以用此生成子段的 params

这种填充动态段的方式被称为“自上而下生成参数”,子段依赖于父段的数据。但如果不依赖,就比如提供一个接口,直接返回所有的产品和对应的目录信息,完全可以直接生成,示例代码如下:

javascript
// app/products/[category]/[product]/page.js
export async function generateStaticParams() {
  const products = await fetch('https://.../products').then((res) => res.json())
 
  return products.map((product) => ({
    category: product.category.slug,
    product: product.id,
  }))
}
 
export default async function Page({ params }) {
  const resolvedParams = await params
  // ...
}

不需要再写父 generateStaticParams 函数,直接一步到位,这种填充动态段的方式被称为“自下而上生成参数”。

1.3. 返回值

generateStaticParams 应该返回一个对象数组,其中每个对象表示单个路由的填充动态段:

  • 对象的每个属性都是路由要填充的动态段
  • 属性名就是段名,属性值就是该段应该填写的内容

直接描述反而有些复杂,其实很简单,比如:

/product/[id]这种动态路由,generateStaticParams 应该返回一个类似于 [{id: xxx}, {id: xxx}, ...] 的对象。

对于 /products/[category]/[product]这种动态路由,generateStaticParams 应该返回一个类似于 [{category: xxx, product: xxx}, {category: xxx, product: xxx}, ...] 的对象。

对于 /products/[...slug]这种动态路由,generateStaticParams 应该返回一个类似于[{slug: [xxx, xxx, ...]}, {slug: [xxx, xxx, ...]}, ...] 的对象。

返回类型描述如下:

示例路由generateStaticParams 返回类型
/product/[id]{ id: string }[]
/products/[category]/[product]{ category: string, product: string }[]
/products/[...slug]{ slug: string[] }[]

2. generateViewport

你可以自定义页面的初始 viewport,有两种方法:

  1. 使用静态的 viewport 对象
  2. 使用动态的 generateViewport 函数

使用的时候要注意:

  1. viewport 对象和 generateViewport 函数仅支持在服务端组件中导出
  2. 不能在同一路由段中同时导出 viewport 对象和 generateViewport 函数
  3. 如果视口不依赖运行时的一些信息,尽可能使用 viewport 对象的方式进行定义

2.1. viewport 对象

layout.js 或者 page.js 中导出一个名为 viewport 的对象:

javascript
// layout.js | page.js 
export const viewport = {
  themeColor: 'black',
}
 
export default function Page() {}

2.2. generateViewport

layout.js 或者 page.js 中导出一个名为 generateViewport 的函数,该函数返回包含一个或者多个viewport 字段的 Viewport 对象:

javascript
export async function generateViewport({ params }) {
  const resolvedParams = await params
  return {
    themeColor: '...',
  }
}

2.3. Viewport 字段

themeColor

theme-color,用户的浏览器将根据所设定的建议颜色来改变用户界面,比如在 Android 上的 Chrome 设定颜色后:

支持简单的主题颜色设置:

javascript
// layout.js | page.js
export const viewport = {
  themeColor: 'black',
}

对应输出为:

html
<meta name="theme-color" content="black" />

也支持带 media 属性的主题颜色设置:

javascript
export const viewport = {
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: 'cyan' },
    { media: '(prefers-color-scheme: dark)', color: 'black' },
  ],
}

对应输出为:

javascript
<meta name="theme-color" media="(prefers-color-scheme: light)" content="cyan" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />

width, initialScale, 和 maximumScale

这其实是 viewport元标签的默认设置值,通常不需要手动设置:

javascript
// layout.js | page.js
export const viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 1,
  // 也支持
  // interactiveWidget: 'resizes-visual',
}

对应输出为:

javascript
<meta
  name="viewport"
  content="width=device-width, initial-scale=1, maximum-scale=1"
/>

colorScheme

colorScheme,指定与当前文档兼容的一种或多种配色方案。 浏览器将优先采用此元数据的值,然后再使用用户的浏览器或设备设置,来确定页面上的各种默认颜色和元素外观,例如背景色、前景色、窗体控件和滚动条。<meta name="color-scheme"> 的主要用途是指示当前页面与浅色模式和深色模式的兼容性,以及选用这两种模式时的优先顺序。它的值有 normallightdarkonly light

javascript
// layout.js | page.js
export const viewport = {
  colorScheme: 'dark',
}
javascript
<meta name="color-scheme" content="dark" />

3. revalidatePath

3.1. 介绍

revalidatePath 用于按需清除特定路径上的缓存数据,可用于 Node.js 和 Edge Runtimes。

使用 revalidatePath 的时候要知道,在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation),即便在动态路由段中调用了多次 revalidatePath,也不会立即触发多次重新验证,只有当下次访问的时候才会重新获取数据并更新缓存。

3.2. 参数

javascript
revalidatePath(path: string, type?: 'page' | 'layout'): void;
  • path 可以是路由字符串(如 /product/123),也可以是文件系统地址字符串(如 /product/[slug]/page),必须少于 1024 个字符
  • type可选参数,要重新验证的地址类型,值为 pagelayout

3.3. 返回值

revalidatePath 不返回任何值

3.4. 示例

重新验证特定 URL

javascript
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/post-1')

重新验证页面路径

javascript
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/[slug]', 'page')
// 带路由组也可以
revalidatePath('/(main)/post/[slug]', 'page')

注意在这个例子中,仅重新验证与所提供的 page 文件对应的 URL,也就是说,不会重新验证在这之下的页面,比如 /blog/[slug] 不会让 /blog/[slug]/[author] 也失效

重新验证布局路径

javascript
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/[slug]', 'layout')
// 带路由组也可以
revalidatePath('/(main)/post/[slug]', 'layout')

在这个例子中,这会何重新验证任何使用这个布局的页面,也就是说, /blog/[slug]也会让 /blog/[slug]/[author] 失效

重新验证所有数据

javascript
import { revalidatePath } from 'next/cache'
 
revalidatePath('/', 'layout')

这会清除客户端路由缓存,并在下次访问时重新验证数据缓存。

Server Action

javascript
'use server'
// app/actions.js
import { revalidatePath } from 'next/cache'
 
export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

路由处理程序

javascript
// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache'
 
export async function GET(request) {
  const path = request.nextUrl.searchParams.get('path')
 
  if (path) {
    revalidatePath(path)
    return Response.json({ revalidated: true, now: Date.now() })
  }
 
  return Response.json({
    revalidated: false,
    now: Date.now(),
    message: 'Missing path to revalidate',
  })
}

4. revalidateTag

4.1. 介绍

revalidateTag 用于按需清除特定标签的缓存数据,可用于 Node.js 和 Edge Runtimes。

使用 revalidateTag 的时候要知道,在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation),即便在动态路由段中调用了多次 revalidateTag,也不会立即触发多次重新验证,只有当下次访问的时候才会重新获取数据并更新缓存。

4.2. 参数

javascript
revalidateTag(tag: string): void;
  • tag 表示要重新验证的标签,必须小于或等于 256 个字符。

添加标签的方式:

javascript
fetch(url, { next: { tags: [...] } });

4.3. 返回值

revalidateTag 不返回任何值

4.4. 示例

Server Action

javascript
// app/actions.js
import { revalidateTag } from 'next/cache'
 
export async function GET(request) {
  const tag = request.nextUrl.searchParams.get('tag')
  revalidateTag(tag)
  return Response.json({ revalidated: true, now: Date.now() })
}

路由处理程序

javascript
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache'
 
export async function GET(request) {
  const tag = request.nextUrl.searchParams.get('tag')
  revalidateTag(tag)
  return Response.json({ revalidated: true, now: Date.now() })
}

5. unstable_cache

5.1. 介绍

unstable_cache 用于缓存昂贵操作的结果(如数据库查询)并在之后的请求中复用结果,使用示例如下:

javascript
import { getUser } from './data';
import { unstable_cache } from 'next/cache';
 
const getCachedUser = unstable_cache(
  async (id) => getUser(id),
  ['my-app-user']
);
 
export default async function Component({ userID }) {
  const user = await getCachedUser(userID);
  ...
}

5.2. 参数

javascript
const data = unstable_cache(fetchData, keyParts, options)()
  • fetchData:获取要缓存数据的异步函数,该函数返回一个 Promise
  • keyParts:用于标识缓存键名的数组,必须包含全局唯一的值
  • options:用于控制缓存行为,具体包含:
    • tags: 用于控制缓存失效的标签数组
    • revalidate:缓存需要重新验证的秒数

5.3. 返回值

unstable_cache 返回一个函数,该函数调用时会返回一个解析为缓存数据的 Promise。如果数据不在缓存中,则会调用提供的函数,将结果缓存并返回。

6. unstable_noStore

6.1. 介绍

unstable_noStore用于声明退出静态渲染和表明该组件不应缓存,使用示例如下:

javascript
import { unstable_noStore as noStore } from 'next/cache';
 
export default async function Component() {
  noStore();
  const result = await db.query(...);
  ...
}

unstable_noStore相当于在 fetch 上添加了 cache: 'no-store'unstable_noStoreexport const dynamic = 'force-dynamic'更好的一点是它更细粒度,可以在每个组件的基础上使用。

6.2. 示例

如果你不想向 fetch 传递额外的选项如 cache: 'no-store'next: { revalidate: 0 },你可以使用 noStore()作为替代。

javascript
import { unstable_noStore as noStore } from 'next/cache';
 
export default async function Component() {
  noStore();
  const result = await db.query(...);
  ...
}

7. useSelectedLayoutSegment

7.1. 介绍

useSelectedLayoutSegment是一个客户端组件 hook,用于读取比调用该方法所在的布局低一级的激活路由段。这个功能对于导航 UI 非常有用,比如父布局中的选项卡,需要根据当前所处的路由段来更改样式,基础使用示例代码如下:

javascript
'use client'
// app/example-client-component.js
import { useSelectedLayoutSegment } from 'next/navigation'
 
export default function ExampleClientComponent() {
  const segment = useSelectedLayoutSegment()
 
  return <p>Active segment: {segment}</p>
}

为了解释这个 hook 的作用和用法,我们来写一个 demo,demo 效果如下:

这个 demo 模拟的是侧边栏点击切换当前文章,你可以看到,随着路由的切换,对应链接的样式也发生了变化。代码如下:

javascript
// app/blog/layout.js
import BlogNavLink from './blog-nav-link'
import getFeaturedPosts from './get-featured-posts'
 
export default async function Layout({ children }) {
  const featuredPosts = await getFeaturedPosts()
  return (
    <div>
      {featuredPosts.map((post) => (
        <div key={post.id}>
          <BlogNavLink slug={post.slug}>{post.title}</BlogNavLink>
        </div>
      ))}
      <div>{children}</div>
    </div>
  )
}
javascript
'use client'
// app/blog/blog-nav-link.js
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
 
export default function BlogNavLink({ slug, children }) {
  const segment = useSelectedLayoutSegment()
  const isActive = slug === segment
 
  return (
    <Link
      href={`/blog/${slug}`}
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {children}
    </Link>
  )
}
javascript
// app/blog/get-featured-posts.js
export default async function getFeaturedPosts() {
  await new Promise((resolve) => setTimeout(resolve, 3000))
  return [
    { id: '1', slug: 'article1', title: '文章 1'},
    { id: '2', slug: 'article2', title: '文章 2'},
    { id: '3', slug: 'article3', title: '文章 3'}
  ]
}
javascript
// app/blog/[slug]/page.js
export default async function Page({ params }) {
  const { slug } = await params
  return <div>当前 slug: {slug}</div>
}

在这个例子中,useSelectedLayoutSegment 是在 app/blog/layout.js这个布局中调用的,所以访问 /blog/article1 的时候,返回的是比这个布局低一级的路由段,也就是会返回 article1,然后我们在 blog-nav-link.js 中根据该返回值和当前 slug 进行判断,从而实现了当前所处链接加粗功能。

useSelectedLayoutSegment返回比调用该方法所在的布局低一级的激活路由段,也就是说,即使你访问 blog/article1/about,因为调用该方法的布局依然是 app/blog/layout.js,所以返回的值依然是 article1

7.2 参数

javascript
const segment = useSelectedLayoutSegment(parallelRoutesKey?: string)

useSelectedLayoutSegment 接收一个可选的 parallelRoutesKey 参数,用于读取平行路由中的激活路由段。

7.3 返回值

如果不存在,会返回 null,让我们再看几个例子:

Layout访问 URL返回值
app/layout.js/null
app/layout.js/dashboard'dashboard'
app/dashboard/layout.js/dashboardnull
app/dashboard/layout.js/dashboard/settings'settings'
app/dashboard/layout.js/dashboard/analytics'analytics'
app/dashboard/layout.js/dashboard/analytics/monthly'analytics'

8. useSelectedLayoutSegments

8.1. 介绍

useSelectedLayoutSegments 是一个客户端组件 hook,用于读取调用该方法所在的布局以下所有的激活路由段。

useSelectedLayoutSegmentsuseSelectedLayoutSegment 的区别是:

  • useSelectedLayoutSegment 返回的是布局下一级的激活路由段
  • useSelectedLayoutSegments 返回的是布局下所有的激活路由段

以上节的 demo 为例,当在 app/blog/layout.js布局中调用这两个方法:

访问 /blog/article1useSelectedLayoutSegment 返回'article1'useSelectedLayoutSegments返回 ['article1']``。

访问 /blog/article1/aboutuseSelectedLayoutSegment返回 'article1'useSelectedLayoutSegments返回 ['article1', 'about']

useSelectedLayoutSegments可以用于实现如面包屑功能,基础使用示例代码如下:

javascript
'use client'
// app/example-client-component.js
import { useSelectedLayoutSegments } from 'next/navigation'
 
export default function ExampleClientComponent() {
  const segments = useSelectedLayoutSegments()
 
  return (
    <ul>
      {segments.map((segment, index) => (
        <li key={index}>{segment}</li>
      ))}
    </ul>
  )
}

8.2. 参数

javascript
const segments = useSelectedLayoutSegments(parallelRoutesKey?: string)

8.3. 返回值

以数组形式返回,如果没有,返回空数组。注意如果使用了路由组,也会返回,所以可以再用一个 filter() 排除掉以括号为开头的条目。让我们再看几个例子:

Layout访问 URL返回值
app/layout.js/[]
app/layout.js/dashboard['dashboard']
app/layout.js/dashboard/settings['dashboard', 'settings']
app/dashboard/layout.js/dashboard[]
app/dashboard/layout.js/dashboard/settings['settings']

参考链接

  1. https://nextjs.org/docs/app/api-reference/functions