> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/next.js-kai-fa-zhi-nan/13.-shu-ju-huo-qu-pian-shu-ju-huo-qu-huan-cun-yu-chong-xin-yan-zheng.md).

# 13. 数据获取篇 ❘ 数据获取、缓存与重新验证

### 前言

在 Next.js 中如何获取数据呢？

Next.js 优先推荐使用原生的 fetch 方法，因为 Next.js 拓展了原生的 fetch 方法，为其添加了缓存和更新缓存(重新验证)的机制。

这样做的好处在于可以自动复用请求数据，提高性能。坏处在于如果你不熟悉，经常会有一些“莫名奇妙”的状况出现……

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

### 1. 服务端使用 fetch

#### 1.1. 基本用法

Next.js 拓展了原生的 [fetch Web API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)，可以为**服务端的每个请求**配置缓存（caching）和重新验证（ revalidating）行为。

你可以在**服务端组件、路由处理程序、Server Actions** 中搭配 `async`/`await` 语法使用 fetch。

举个例子：

```javascript
// app/page.js
async function getData() {
  const res = await fetch('https://jsonplaceholder.typicode.com/todos')
  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>{JSON.stringify(data)}</main>
}
```

#### 1.2. 默认缓存

默认情况下，Next.js 会自动缓存服务端 `fetch` 请求的返回值（背后用的是[数据缓存（Data Cache）](https://juejin.cn/book/7307859898316881957/section/7309077169735958565#heading-6)）。

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

但这些情况默认不会自动缓存：

1. 在 Server Action 中使用的时候
2. 在定义了非 GET 方法的路由处理程序中使用的时候

**简单的来说，在服务端组件和只有 GET 方法的路由处理程序中使用 fetch，返回结果会自动缓存。**

**1.2.1. logging 配置项**

让我们分别举个例子演示下。但在写代码之前，先让我们修改下 `next.config.mjs` 的配置：

```js
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true
    }
  }
};

export default nextConfig;
```

目前 logging 只有这一个配置，用于**在开发模式下**显示 fetch 请求和缓存日志：

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76fca0ddd8f54a12bae3b9a0f6487f90~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1078\&h=78\&s=71180\&e=png\&b=070314)

上图日志的意思是：

访问 `/api/cache` 路由，其中 GET 请求了 <https://dog.ceo/api/breeds/image/random> 这个接口，接口 20ms 返回，状态码 200，此次请求命中了缓存（HIT）。

这个日志会帮助我们查看缓存情况（实际用的时候有的日志结果不是很准，还有待改进）。

**1.2.2. 服务端组件**

第一种在服务端组件中使用，修改 `app/page.js`，代码如下：

```js
async function getData() {
  // 接口每次调用都会返回一个随机的猫猫图片数据
  const res = await fetch('https://api.thecatapi.com/v1/images/search')
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}

export default async function Page() {
  const data = await getData()
  
  return <img src={data[0].url} width="300" />
}
```

运行 `npm run dev`，开启开发模式：

![cache-4.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09ea27349964498e81fc2a196ae174c8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1436\&h=459\&s=466881\&e=gif\&f=39\&b=180831)

在开发模式下，为了方便调试，可以使用浏览器的硬刷新（Command + Shift + R）清除缓存，此时数据会发生更改（cache: SKIP）。普通刷新时因为会命中缓存（cache: HIT），数据会保持不变。

运行 `npm run build && npm run start` 开启生产版本：

![cache.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e26d6e09a5e04b99bba03a8a1f9d8141~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=439\&h=353\&s=136348\&e=gif\&f=33\&b=efedec)

因为 fetch 请求的返回结果被缓存了，无论是否硬刷新，图片数据都会保持不变。

**1.2.3. 路由处理程序 GET 请求**

第二种在路由处理程序中使用，新建 `app/api/cache/route.js`，代码如下：

```js
export async function GET() {
  const res = await fetch('https://dog.ceo/api/breeds/image/random')
  
  const data = await res.json()
  return Response.json({ data })
}
```

运行 `npm run dev`，开启开发模式：

![cache-5.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c844d6e5ce04083b81e5797241b7494~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1436\&h=459\&s=293876\&e=gif\&f=40\&b=141414)

开发模式下，浏览器硬刷新的时候会跳过缓存，普通刷新的时候则会命中缓存。可以看到第一次硬刷新的时候，请求接口时间为 912ms，后面普通刷新的时候，因为使用缓存中的数据，数据返回时间都是 1ms 左右。

运行 `npm run build && npm run start` 开启生产版本：

![cache-6.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a5d4476224694a73a8f631359cd2ead6~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1036\&h=192\&s=53493\&e=gif\&f=31\&b=191919)

因为 fetch 请求的返回结果被缓存了，无论是否硬刷新，接口数据都会保持不变。

#### 1.3. 重新验证

**在 Next.js 中，清除数据缓存并重新获取最新数据的过程就叫做重新验证（Revalidation）。**

Next.js 提供了两种方式重新验证：

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

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

**基于时间的重新验证**

使用基于时间的重新验证，你需要在使用 fetch 的时候设置 `next.revalidate` 选项（以秒为单位）：

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

或者通过[路由段配置项](https://juejin.cn/book/7307859898316881957/section/7309079033223446554)进行配置，使用这种方法，它会重新验证该路由段所有的 `fetch` 请求。

```javascript
// layout.jsx | page.jsx | route.js
export const revalidate = 3600
```

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

**按需重新验证**

使用按需重新验证，在**路由处理程序或者 Server Action** 中通过路径（ [revalidatePath](https://juejin.cn/book/7307859898316881957/section/7309079586296791050#heading-12)） 或缓存标签 [revalidateTag](https://juejin.cn/book/7307859898316881957/section/7309079586296791050#heading-23) 实现。

**revalidatePath**

新建 `app/api/revalidatePath/route.js`，代码如下：

```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',
  })
}
```

此时访问 `/api/revalidatePath?path=/` 就会更新 `/` 的 fetch 请求返回数据，交互效果如下：

![cache-7.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3b0fbd05293b4b58bc8d5dee6b8182b7~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=913\&h=380\&s=533495\&e=gif\&f=141\&b=f7f4f3)

此时访问 `/api/revalidatePath?path=/api/cache` 就会更新 `/api/cache` 的 fetch 请求返回数据，交互效果如下：

![cache-8.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ae60a8b621644c9a45ea99378260a89~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1035\&h=370\&s=305075\&e=gif\&f=108\&b=171717)

注意：这两张图演示的是开发模式下的情况，用 revalidatePath 确实更新了对应路径上的 fetch 缓存结果。但如果大家部署到生产版本，你是发现 revalidatePath 只对页面生效，对路由处理程序并不生效。

这是因为 `/api/cache` 被静态渲染了，首先你要将 `/api/cache` 转为动态渲染，然后才能测试 revalidatePath 的效果。但是转为动态渲染，比如使用 cookies 等函数，又会触发 Next.js 的自动逻辑，让 fetch 请求退出缓存。

简而言之，如果你想在生产环境测试 revalidatePath 对路由处理程序的影响，你需要多做一些配置：

```js
// 路由动态渲染
export const revalidate = 0
// fetch 强制缓存
export const fetchCache = 'force-cache'
export async function GET() {
  const res = await fetch('https://dog.ceo/api/breeds/image/random')
  
  const data = await res.json()
  return Response.json({ data, now: Date.now() })
}
```

这样的代码在生产环境下，是可以被 revalidatePath 重新验证的。效果同开发模式下的截图。

**revalidateTag**

Next.js 有一个路由标签系统，可以跨路由实现多个 fetch 请求重新验证。具体这个过程为：

1. 使用 fetch 的时候，设置一个或者多个标签标记请求
2. 调用 revalidateTag 方法重新验证该标签对应的所有请求

举个例子：

```javascript
// 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 请求重新验证。

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

让我们真的写个例子。修改 `app/page.js` 代码如下：

```js
async function getData() {
  const res = await fetch('https://api.thecatapi.com/v1/images/search', { next: { tags: ['collection'] } })
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}

export default async function Page() {
  const data = await getData()
  
  return <img src={data[0].url} width="300" />
}
```

修改 `app/api/cache/route.js`，代码如下：

```js
export const revalidate = 0
export const fetchCache = 'force-cache'

export async function GET() {
  const res = await fetch('https://dog.ceo/api/breeds/image/random', { next: { tags: ['collection'] } })
  
  const data = await res.json()
  return Response.json({ data, now: Date.now() })
}
```

新建 `app/api/revalidateTag/route.js`，代码如下：

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

此时访问 `/api/revalidateTag?tag=collection` 就会让 `/` 页面和 `/api/cache` 接口的数据都重新验证：

![cache-9.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/86ff118bb5034674a986d2acc03e9c57~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1042\&h=493\&s=552326\&e=gif\&f=126\&b=fbf8f8)

**错误处理和重新验证**

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

#### 1.4. 退出数据缓存

当 `fetch` 请求满足这些条件时都会退出数据缓存：

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

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

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

不缓存多个请求，可以借助[路由段配置项](https://juejin.cn/book/7307859898316881957/section/7309079033223446554)：

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

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

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

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

举个例子：

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

现在我们调用两次 `getItem` ：

```javascript
// 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)
  // ...
}
```

```javascript
// app/item/[id]/page.js
import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600
 
export default async function Page({ params: { id } }) {
  const item = await getItem(id)
  // ...
}
```

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

注：这里的代码并不是完整可运行的，如果想要细致了解 React Cache 函数的特性，可以查看： [（技巧）当 Next.js 遇到频繁重复的数据库操作时，记住使用 React 的 cache 函数](https://juejin.cn/post/7348643498117038099#heading-5)

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

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

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

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

你也可以在客户端使用三方的库如 [SWR](https://swr.vercel.app/) 或 [React Query](https://tanstack.com/query/latest) 来获取数据。这些库都有提供自己的 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](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e0271e01ad344ceeaa4059be34513311~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1600\&h=785\&s=475591\&e=png\&b=151515)

注：关于 Suspense 和 Streaming，我们会在 [《渲染篇 | Suspense 与 Streaming》](https://juejin.cn/book/7307859898316881957/section/7342436717142409242) 中详细讲解。

#### 5.4. 串行获取数据

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

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1731c628568e4746957939c94929c309~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1600\&h=525\&s=338982\&e=png\&b=141414)

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

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

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

```javascript
// 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>` 组件，展示一个即时加载状态，防止整个路由被数据请求阻塞，而且用户还可以与未被阻塞的部分进行交互。

关于阻塞数据请求：

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

#### 5.5. 并行数据请求

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

```javascript
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>
    </>
  )
}
```

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

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

#### 5.6. 预加载数据

防止出现串行请求的另外一种方式是使用预加载。举个例子：

```javascript
// app/article/[id]/page.js
import Article, { preload, checkIsAvailable } from './components/Article'
 
export default async function Page({ params: { id } }) {
  // 获取文章数据
  preload(id)
  // 执行另一个异步任务，这里是伪代码，比如判断文章是否有权限访问
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Article id={id} /> : null
}
```

而在具体的 preload 函数中，则要搭配 React 的 cache 函数一起使用：

```javascript
// components/Article.js
import { getArticle } from '@/utils/get-article'
import { cache } from 'react'

export const getArticle = cache(async (id) => {
  // ...
})

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

export const checkIsAvailable = (id) => {
	// ...
}

export default async function Article({ id }) {
  const result = await getArticle(id)
  // ...
}
```

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

你可以将 `cache` 函数，`preload` 模式和 [server-only](https://juejin.cn/book/7307859898316881957/section/7309076661532622885#heading-15) 包一起使用，创建一个可在整个应用使用的数据请求工具函数。

```javascript
// utils/get-article.js
import { cache } from 'react'
import 'server-only'
 
export const preloadArticle = (id) => {
  void getArticle(id)
}
 
export const getArticle = cache(async (id) => {
  // ...
})
```

现在，你可以提前获取数据、缓存返回结果，并保证数据获取只发生在服务端。此外，布局、页面、组件都可以使用 `utils/get-article.js`

注：如果想要细致了解 preload 函数和 server-only 以及 cache 的特性，可以查看： [（技巧）当 Next.js 遇到频繁重复的数据库操作时，记住使用 React 的 cache 函数](https://juejin.cn/post/7348643498117038099#heading-5)

### 小结

恭喜你，完成了本篇内容的学习！

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

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

### 参考链接

1. [Data Fetching: Fetching, Caching, and Revalidating | Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating)
2. [Data Fetching: Data Fetching Patterns | Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/patterns)
