跳到内容
构建你的应用数据获取数据获取和缓存

数据获取和缓存

示例

本指南将带您了解 Next.js 中数据获取和缓存的基础知识,并提供实用示例和最佳实践。

这是 Next.js 中数据获取的最小示例。

app/page.tsx
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

此示例演示了在异步 React 服务端组件中使用 `fetch` API 的基本服务端数据获取。

参考

示例

在服务器上使用 `fetch` API 获取数据

此组件将获取并显示博客文章列表。默认情况下,来自 `fetch` 的响应不会被缓存。

app/page.tsx
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

如果您在此路由的其他任何地方都没有使用任何动态 API,则它将在 `next build` 期间预渲染为静态页面。然后可以使用增量静态再生来更新数据。

要防止页面预渲染,您可以将以下内容添加到您的文件中

export const dynamic = 'force-dynamic'

但是,您通常会使用诸如 `cookies`、`headers` 或从页面 props 读取传入的 `searchParams` 等函数,这些函数将自动使页面动态渲染。在这种情况下,您*不需要*显式使用 `force-dynamic`。

在服务器上使用 ORM 或数据库获取数据

此组件将获取并显示博客文章列表。默认情况下,来自数据库的响应不会被缓存,但可以通过其他配置进行缓存。

app/page.tsx
import { db, posts } from '@/lib/db'
 
export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

如果您在此路由的其他任何地方都没有使用任何动态 API,则它将在 `next build` 期间预渲染为静态页面。然后可以使用增量静态再生来更新数据。

要防止页面预渲染,您可以将以下内容添加到您的文件中

export const dynamic = 'force-dynamic'

但是,您通常会使用诸如 `cookies`、`headers` 或从页面 props 读取传入的 `searchParams` 等函数,这些函数将自动使页面动态渲染。在这种情况下,您*不需要*显式使用 `force-dynamic`。

在客户端获取数据

我们建议首先尝试在服务器端获取数据。

但是,在某些情况下,客户端数据获取仍然有意义。在这些情况下,您可以手动在 `useEffect` 中调用 `fetch`(不建议),或者依靠社区中流行的 React 库(例如 SWR 或 React Query)进行客户端获取。

app/page.tsx
'use client'
 
import { useState, useEffect } from 'react'
 
export function Posts() {
  const [posts, setPosts] = useState(null)
 
  useEffect(() => {
    async function fetchPosts() {
      const res = await fetch('https://api.vercel.app/blog')
      const data = await res.json()
      setPosts(data)
    }
    fetchPosts()
  }, [])
 
  if (!posts) return <div>Loading...</div>
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

使用 ORM 或数据库缓存数据

您可以使用 `unstable_cache` API 在运行 `next build` 时缓存响应。

app/page.tsx
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
 
const getPosts = unstable_cache(
  async () => {
    return await db.select().from(posts)
  },
  ['posts'],
  { revalidate: 3600, tags: ['posts'] }
)
 
export default async function Page() {
  const allPosts = await getPosts()
 
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

此示例将数据库查询结果缓存 1 小时(3600 秒)。它还添加了缓存标签 `posts`,然后可以使用增量静态再生来使其失效。

在多个函数之间重用数据

Next.js 使用诸如 `generateMetadata` 和 `generateStaticParams` 之类的 API,您需要在 `page` 中使用相同的数据。

如果您正在使用 `fetch`,则可以通过添加 `cache: 'force-cache'` 来记忆请求。这意味着您可以安全地使用相同的选项调用相同的 URL,并且只会发出一个请求。

须知

  • 在以前版本的 Next.js 中,使用 `fetch` 将具有默认的 `cache` 值 `force-cache`。这在版本 15 中发生了变化,默认值为 `cache: no-store`。
app/blog/[id]/page.tsx
import { notFound } from 'next/navigation'
 
interface Post {
  id: string
  title: string
  content: string
}
 
async function getPost(id: string) {
  const res = await fetch(`https://api.vercel.app/blog/${id}`, {
    cache: 'force-cache',
  })
  const post: Post = await res.json()
  if (!post) notFound()
  return post
}
 
export async function generateStaticParams() {
  const posts = await fetch('https://api.vercel.app/blog', {
    cache: 'force-cache',
  }).then((res) => res.json())
 
  return posts.map((post: Post) => ({
    id: String(post.id),
  }))
}
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return {
    title: post.title,
  }
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

如果您*没有*使用 `fetch`,而是直接使用 ORM 或数据库,则可以使用 React `cache` 函数包装您的数据获取。这将去重并仅进行一次查询。

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db' // Example with Drizzle ORM
import { notFound } from 'next/navigation'
 
export const getPost = cache(async (id) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
 
  if (!post) notFound()
  return post
})

重新验证缓存数据

了解有关使用增量静态再生重新验证缓存数据的更多信息。

模式

并行和串行数据获取

在组件内部获取数据时,您需要注意两种数据获取模式:并行和串行。

Sequential and Parallel Data Fetching
  • 串行:组件树中的请求彼此依赖。这可能会导致更长的加载时间。
  • 并行:路由中的请求会被急切地启动,并将同时加载数据。这减少了加载数据所需的总时间。

串行数据获取

如果您有嵌套组件,并且每个组件都获取自己的数据,那么如果这些数据请求没有被记忆,则数据获取将按顺序进行。

在某些情况下,您可能需要此模式,因为一个获取依赖于另一个获取的结果。例如,`Playlists` 组件只有在 `Artist` 组件完成数据获取后才会开始获取数据,因为 `Playlists` 依赖于 `artistID` prop

app/artist/[username]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // Get artist information
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* Show fallback UI while the Playlists component is loading */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* Pass the artist ID to the Playlists component */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
 
async function Playlists({ artistID }: { artistID: string }) {
  // Use the artist ID to fetch playlists
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

您可以使用 `loading.js`(用于路由段)或 React ``(用于嵌套组件)来显示即时加载状态,同时 React 流式传输结果。

这将防止整个路由被数据请求阻塞,并且用户将能够与页面中已准备好的部分进行交互。

并行数据获取

默认情况下,布局和页面段并行渲染。这意味着请求将并行启动。

但是,由于 `async`/`await` 的性质,同一段或组件内部等待的请求将阻止其下方的任何请求。

要并行获取数据,您可以通过在组件外部定义请求来急切地启动请求,这些组件使用数据。这通过并行启动两个请求来节省时间,但是,用户在两个 Promise 都解决之前不会看到渲染结果。

在下面的示例中,`getArtist` 和 `getAlbums` 函数在 `Page` 组件外部定义,并在组件内部使用 `Promise.all` 启动。

app/artist/[username]/page.tsx
import Albums from './albums'
 
async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}
 
async function getAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)
 
  // Initiate both requests in parallel
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  )
}

此外,您可以添加 Suspense Boundary 来分解渲染工作并尽快显示部分结果。

预加载数据

另一种防止瀑布流的方法是使用预加载模式,方法是创建一个实用程序函数,您可以在阻止请求之上急切地调用该函数。例如,`checkIsAvailable()` 阻止 `` 渲染,因此您可以在其之前调用 `preload()` 以急切地启动 `` 数据依赖项。当 `` 渲染时,其数据已被获取。

请注意,`preload` 函数不会阻止 `checkIsAvailable()` 运行。

components/Item.tsx
import { getItem } from '@/utils/get-item'
 
export const preload = (id: string) => {
  // void evaluates the given expression and returns undefined
  // https://mdn.org.cn/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
app/item/[id]/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}

须知: “preload” 函数也可以有任何名称,因为它是一种模式,而不是 API。

将 React `cache` 和 `server-only` 与预加载模式一起使用

您可以将 `cache` 函数、`preload` 模式和 `server-only` 包组合起来,创建一个可在整个应用程序中使用的数据获取实用程序。

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

使用这种方法,您可以急切地获取数据、缓存响应,并保证此数据获取仅在服务器上发生。

`utils/get-item` 导出可以由布局、页面或其他组件使用,以使它们控制何时获取项目的数据。

须知

  • 我们建议使用 `server-only` 包来确保服务器数据获取函数永远不会在客户端使用。

防止敏感数据暴露给客户端

我们建议使用 React 的 taint API,taintObjectReferencetaintUniqueValue,以防止整个对象实例或敏感值传递给客户端。

要在您的应用程序中启用 tainting,请将 Next.js 配置 experimental.taint 选项设置为 `true`。

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

然后将您要 taint 的对象或值传递给 `experimental_taintObjectReference` 或 `experimental_taintUniqueValue` 函数。

app/utils.ts
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Do not pass the whole user object to the client',
    data
  )
  experimental_taintUniqueValue(
    "Do not pass the user's address to the client",
    data,
    data.address
  )
  return data
}
app/page.tsx
import { getUserData } from './data'
 
export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // this will cause an error because of taintObjectReference
      address={userData.address} // this will cause an error because of taintUniqueValue
    />
  )
}