JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第33章—API篇常用函数与方法(下)

首次发表于 2024-03-22, 更新于 2024-03-22

前言

本篇我们讲解请求相关的常用方法,有:

  1. generateStaticParams
  2. generateViewport
  3. revalidatePath
  4. revalidateTag
  5. unstable_cache
  6. unstable_noStore
  7. useSelectedLayoutSegment
  8. useSelectedLayoutSegments

用到的时候到此篇查看具体的语法即可。

1. generateStaticParams

1.1. 介绍

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

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

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

// 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 function Page({ params }) {
  const { slug } = params
  // ...
}

关于 generateStaticParams

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

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

// 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 function Page({ params }) {
  const { category, product } = params
  // ...
}

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

// 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 function Page({ params }) {
  const { slug } = params
  // ...
}

1.2. 参数

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

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

首先生成父段:

// 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 function Layout({ params }) {
  // ...
}

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

// 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 function Page({ params }) {
  // ...
}

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

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

// 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 function Page({ 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 的对象:

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

2.2. generateViewport

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

export function generateViewport({ params }) {
  return {
    themeColor: '...',
  }
}

2.3. Viewport 字段

themeColor

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

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

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

对应输出为:

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

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

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

对应输出为:

<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元标签的默认设置值,通常不需要手动设置:

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

对应输出为:

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

colorScheme

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

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

3. revalidatePath

3.1. 介绍

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

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

3.2. 参数

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

3.3. 返回值

revalidatePath 不返回任何值

3.4. 示例

重新验证特定 URL

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

重新验证页面路径

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

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

重新验证布局路径

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

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

重新验证所有数据

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

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

Server Action

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

路由处理程序

// 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. 参数

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

添加标签的方式:

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

4.3. 返回值

revalidateTag 不返回任何值

4.4. 示例

Server Action

// 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() })
}

路由处理程序

// 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 用于缓存昂贵操作的结果(如数据库查询)并在之后的请求中复用结果,使用示例如下:

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. 参数

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用于声明退出静态渲染和表明该组件不应缓存,使用示例如下:

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()作为替代。

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 非常有用,比如父布局中的选项卡,需要根据当前所处的路由段来更改样式,基础使用示例代码如下:

'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 效果如下:

1115.gif

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

// 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>
  )
}
'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>
  )
}
// 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'}
  ]
}
// app/blog/[slug]/page.js
export default function Page({ params }) {
  return <div>当前 slug: {params.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 参数

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 /dashboard null
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可以用于实现如面包屑功能,基础使用示例代码如下:

'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. 参数

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
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru