JackyLove 的技术人生

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

第12章—数据获取篇数据获取、缓存与重新验证

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

前言

在 Next.js 中如何获取数据呢?Next.js 优先推荐使用原生的 fetch 方法,因为 Next.js 拓展了原生的 fetch 方法,为其添加了缓存和更新缓存(重新验证)的机制,这样做的好处在于可以自动复用请求数据,提高性能。坏处在于你得多学一点关于如何更新缓存……

那就让我们来看看具体如何使用吧。

1. 服务端使用 fetch

Next.js 拓展了原生的 fetch Web API,增加了缓存(caching)和重新验证( revalidating)功能。你可以在:

  1. 服务端组件
  2. 路由处理程序
  3. Server Actions

中搭配 async/await 使用 fetch。举个例子:

// app/page.js
async function getData() {
  const res = await fetch('https://api.example.com/...') 
  if (!res.ok) {
    // 这会触发最近的 `error.js` 错误边界
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return <main></main>
}

1.1. 缓存数据

默认情况下,Next.js 会自动缓存服务端 fetch 的返回值,也就是说,数据会在构建或者请求的时候被缓存,后续相同的请求会直接使用缓存中的数据,这有利于提高应用的性能。

// fetch 的 cache 选项用于控制该请求的缓存行为
// 默认就是 'force-cache', 平时写的时候可以省略
fetch('https://...', { cache: 'force-cache' })

注:不仅 GET 请求会被缓存,正常使用 POST 方法的 fetch 请求也会被自动缓存,但在路由处理程序中使用 POST 方法的 fetch 请求不会被缓存。

1.2. 重新验证

有的时候缓存数据会过期,那么该如何更新缓存呢?在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation)。

Next.js 提供了两种方式更新缓存:

一种是基于时间的重新验证(Time-based revalidation),即经过一定时间并有新请求产生后重新验证数据,适用于不经常更改且新鲜度不那么重要的数据。

一种是按需重新验证(On-demand revalidation),根据事件手动重新验证数据。按需重新验证又可以使用基于标签(tag-based)和基于路径(path-based)两种方法重新验证数据。适用于需要尽快展示最新数据的场景。

基于时间的重新验证

使用基于时间的重新验证,你需要在使用 fetch 的时候设置 next.revalidate 选项(以秒为单位):

fetch('https://...', { next: { revalidate: 3600 } })

或者通过路由段配置项进行配置,使用这种方法,它会重新验证该路由段所有的 fetch 请求。

// layout.js | page.js
export const revalidate = 3600

注:在一个静态渲染的路由中,如果你有多个请求,每个请求设置了不同的重新验证时间,将会使用最短的时间用于所有的请求。而对于动态渲染的路由,每一个 fetch请求都将独立重新验证。

按需重新验证

使用按需重新验证,在路由处理程序或者 Server Action 中通过路径( revalidatePath) 或缓存标签 revalidateTag 实现。

Next.js 有一个路由标签系统,可以跨路由实现多个 fetch 请求重新验证。我们来具体介绍下这个过程:

  1. 当你使用 fetch 的时候,你可以使用设置一个或者多个标签来标记请求
  2. 然后你就可以调用 revalidateTag方法重新验证该标签对应的所有的请求

举个例子:

// app/page.js
export default async function Page() {
  const res = await fetch('https://...', { next: { tags: ['collection'] } })
  const data = await res.json()
  // ...
}

在这个例子中,你为 fetch 请求添加了一个 collection标签。现在,你可以在 Server Action 中调用 revalidateTag,就可以让所有带 collection 标签的 fetch 请求重新验证。

// app/actions.js
'use server'
 
import { revalidateTag } from 'next/cache'
 
export default async function action() {
  revalidateTag('collection')
}

错误处理和重新验证

如果在尝试重新验证的过程中出现错误,缓存会继续提供上一个重新生成的数据,而在下一个后续请求中,Next.js 会尝试再次重新验证数据。

1.3. 退出数据缓存

fetch 请求满足这些条件时不会被缓存:

  • fetch 请求添加了 cache: 'no-store' 选项
  • fetch 请求添加了 revalidate: 0 选项
  • fetch 请求在路由处理程序中并使用了 POST 方法
  • 使用headerscookies 的方法之后使用 fetch请求
  • 配置了路由段选项 const dynamic = 'force-dynamic'
  • 配置了路由段选项fetchCache ,默认会跳过缓存
  • fetch 请求使用了 Authorization或者 Cookie请求头,并且在组件树中其上方还有一个未缓存的请求

在具体使用的时候,如果你想不缓存某个单独请求:

// layout.js | page.js
fetch('https://...', { cache: 'no-store' })

不缓存多个请求,可以借助路由段配置项

// layout.js | page.js
export const dynamic = 'force-dynamic'

Next.js 推荐单独配置每个请求的缓存行为,这可以让你更精细化的控制缓存行为。

2.服务端使用三方请求库

也不是所有时候都能使用 fetch 请求,如果你使用了不支持或者暴露 fetch 方法的三方库(如数据库、CMS 或 ORM 客户端),但又想实现数据缓存机制,那你可以使用 React 的 cache 函数和路由段配置项来实现请求的缓存和重新验证。

举个例子:

// app/utils.js
import { cache } from 'react'
 
export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ id })
  return item
})

现在我们调用两次 getItem

// app/item/[id]/layout.js
import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600 // 最多每小时重新验证一次
 
export default async function Layout({ params: { id } }) {
  const item = await getItem(id)
  // ...
}
// app/item/[id]/page.js
import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600 // revalidate the data at most every hour
 
export default async function Page({ params: { id } }) {
  const item = await getItem(id)
  // ...
}

在这个例子中,尽管 getItem 被调用两次,但只会产生一次数据库查询。

3. 客户端使用路由处理程序

如果你需要在客户端组件中获取数据,可以在客户端调用路由处理程序。路由处理程序会在服务端被执行,然后将数据返回给客户端,适用于不想暴露敏感信息给客户端(比如 API tokens)的场景。

如果你使用的是服务端组件,无须借助路由处理程序,直接获取数据即可。

4. 客户端使用三方请求库

你也可以在客户端使用三方的库如 SWRReact Query 来获取数据。这些库都有提供自己的 API 实现记忆请求、缓存、重新验证和更改数据。

5. 建议与最佳实践

有一些在 React 和 Next.js 中获取数据的建议和最佳实践,本节来介绍一下:

5.1. 尽可能在服务端获取数据

尽可能在服务端获取数据,这样做有很多好处,比如:

  1. 可以直接访问后端资源(如数据库)
  2. 防止敏感信息泄漏
  3. 减少客户端和服务端之间的来回通信,加快响应时间
  4. ...

5.2. 在需要的地方就地获取数据

如果组件树中的多个组件使用相同的数据,无须先全局获取,再通过 props 传递,你可以直接在需要的地方使用 fetch 或者 React cache 获取数据,不用担心多次请求造成的性能问题,因为 fetch 请求会自动被记忆化。这也同样适用于布局,毕竟本来父子布局之间也不能传递数据。

5.3. 适当的时候使用 Streaming

Streaming 和 Suspense都是 React 的功能,允许你增量传输内容以及渐进式渲染 UI 单元。页面可以直接渲染部分内容,剩余获取数据的部分会展示加载态,这也意味着用户不需要等到页面完全加载完才能与其交互。

image.png

关于 Streaming,我们会在小册 《渲染篇 | Streaming 和 Edge Runtime》详细讲解。

5.4. 串行获取数据

在 React 组件内获取数据时,有两种数据获取模式,并行和串行。

image.png

所谓串行数据获取,数据请求相互依赖,形成瀑布结构,这种行为有的时候是必要的,但也会导致加载时间更长。

所谓并行数据获取,请求同时发生并加载数据,这会减少加载数据所需的总时间。

我们先说说串行数据获取,直接举个例子:

// app/artist/page.js
// ...
 
async function Playlists({ artistID }) {
  // 等待 playlists 数据
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}
 
export default async function Page({ params: { username } }) {
  // 等待 artist 数据
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

在这个例子中,Playlists 组件只有当 Artist 组件获得数据才会开始获取数据,因为 Playlists 组件依赖 artistId 这个 prop。这也很容易理解,毕竟只有先知道了是哪位艺术家,才能获取这位艺术家对应的曲目。

在这种情况下,你可以使用 loading.js 或者 React 的 <Suspense> 组件,展示一个即时加载状态,防止整个路由被数据请求阻塞,而且用户还可以与未被阻塞的部分进行交互。

关于阻塞数据请求:

  • 一种防止出现串行数据请求的方法是在应用程序根部全局获取数据,但这会阻塞其下所有路由段的渲染,直到数据加载完毕。
  • 任何使用 awaitfetch 请求都会阻塞渲染和下方所有组件的数据请求,除非它们使用了 <Suspense> 或者 loading.js。另一种替代方式就是使用并行数据请求或者预加载模式。

5.5. 并行数据请求

要实现并行请求数据,你可以在使用数据的组件外定义请求,然后在组价内部调用,举个例子:

import Albums from './albums'

// 组件外定义
async function getArtist(username) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}
 
async function getArtistAlbums(username) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}
 
export default async function Page({ params: { username } }) {
  // 组件内调用,这里是并行的
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)
 
  // 等待 promise resolve
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

在这个例子中,getArtistgetArtistAlbums 函数都是在 Page 组件外定义,然后在 Page 组件内部调用。用户需要等待两个 promise 都 resolve 后才能看到结果。

为了提升用户体验,可以使用 Suspense 组件来分解渲染工作,尽快展示出部分结果。

5.6. 预加载数据

防止出现串行请求的另外一种方式是使用预加载。你可以创建一个 preload 函数进一步优化并行数据获取。使用这种方式,你不需要再使用 props 往下传递,举个例子:

// components/Item.js
import { getItem } from '@/utils/get-item'

export const preload = (id) => {
	void getItem(id)
}

export default async function Item({ id }) {
  const result = await getItem(id)
  // ...
}
// app/item/[id]/page.js
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({ params: { id } }) {
  // 开始加载 item 数据
  preload(id)
  // 执行另一个异步任务
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}

5.7. 使用 React cache server-only 和预加载模式

你可以将 cache 函数,preload 模式和 server-only 包一起使用,创建一个可在整个应用使用的数据请求工具函数。

// utils/get-item.js
import { cache } from 'react'
import 'server-only'
 
export const preload = (id) => {
  void getItem(id)
}
 
export const getItem = cache(async (id) => {
  // ...
})

使用这种方式,你可以快速获取数据、缓存返回结果并保证数据获取只发生在服务端。布局、页面或其他组件可以使用 utils/get-item

小结

恭喜你,完成了本篇内容的学习!

这一节我们介绍了请求数据的四种方式,重点介绍了服务端使用 fetch 的方式,这是因为 Next.js 拓展了原生的 fetch,增加了数据缓存和重新验证的逻辑。在 Next.js 中,为了提高性能,应该尽可能的使用缓存,但为了保证数据的时效性,也应该设置合理的重新验证逻辑。Next.js 推荐单独配置每个请求的缓存行为,这可以让你更精细化的控制缓存行为。

介绍完四种数据请求方式后,Next.js 提供了一些获取数据的建议和最佳实践,正是因为有了强大的缓存功能,所以在书写代码的时候可以就地获取数据,而不用担心相同请求多次发送造成的性能影响。

参考链接

  1. Data Fetching: Fetching, Caching, and Revalidating | Next.js
  2. Data Fetching: Data Fetching Patterns | Next.js
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru