数据获取和缓存
Examples
本指南将引导你了解 Next.js 中数据提取和缓存的基础知识,并提供实际示例和最佳实践。
¥This guide will walk you through the basics of data fetching and caching in Next.js, providing practical examples and best practices.
以下是 Next.js 中数据提取的一个最小示例:
¥Here's a minimal example of data fetching in Next.js:
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
此示例演示了在异步 React 服务器组件中使用 fetch
API 进行基本的服务器端数据提取。
¥This example demonstrates a basic server-side data fetch using the fetch
API in an asynchronous React Server Component.
参考
¥Reference
-
React
cache
-
Next.js
unstable_cache
示例
¥Examples
使用 fetch
API 在服务器上获取数据
¥Fetching data on the server with the fetch
API
此组件将获取并显示博客文章列表。默认情况下,fetch
的响应不会被缓存。
¥This component will fetch and display a list of blog posts. The response from fetch
is not cached by default.
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
如果你未在此路由中的其他任何地方使用任何 动态 API,它将在 next build
期间预渲染为静态页面。然后可以使用 增量静态再生 更新数据。
¥If you are not using any Dynamic APIs anywhere else in this route, it will be prerendered during next build
to a static page. The data can then be updated using Incremental Static Regeneration.
为防止页面预渲染,你可以将以下内容添加到文件中:
¥To prevent the page from prerendering, you can add the following to your file:
export const dynamic = 'force-dynamic'
但是,你通常会使用 cookies
、headers
等函数,或从页面 props 读取传入的 searchParams
,这将自动使页面动态渲染。在这种情况下,你不需要明确使用 force-dynamic
。
¥However, you will commonly use functions like cookies
, headers
, or reading the incoming searchParams
from the page props, which will automatically make the page render dynamically. In this case, you do not need to explicitly use force-dynamic
.
使用 ORM 或数据库在服务器上获取数据
¥Fetching data on the server with an ORM or database
此组件将获取并显示博客文章列表。默认情况下,数据库的响应不会被缓存,但可以使用 附加配置。
¥This component will fetch and display a list of blog posts. The response from the database is not cached by default but could be with additional configuration.
import { db, posts } from '@/lib/db'
export default async function Page() {
let allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
import { db, posts } from '@/lib/db'
export default async function Page() {
let allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
如果你未在此路由中的其他任何地方使用任何 动态 API,它将在 next build
期间预渲染为静态页面。然后可以使用 增量静态再生 更新数据。
¥If you are not using any Dynamic APIs anywhere else in this route, it will be prerendered during next build
to a static page. The data can then be updated using Incremental Static Regeneration.
为防止页面预渲染,你可以将以下内容添加到文件中:
¥To prevent the page from prerendering, you can add the following to your file:
export const dynamic = 'force-dynamic'
但是,你通常会使用 cookies
、headers
等函数,或从页面 props 读取传入的 searchParams
,这将自动使页面动态渲染。在这种情况下,你不需要明确使用 force-dynamic
。
¥However, you will commonly use functions like cookies
, headers
, or reading the incoming searchParams
from the page props, which will automatically make the page render dynamically. In this case, you do not need to explicitly use force-dynamic
.
在客户端上获取数据
¥Fetching data on the client
我们建议首先尝试在服务器端获取数据。
¥We recommend first attempting to fetch data on the server-side.
但是,在某些情况下,客户端数据获取是有意义的。在这些情况下,你可以在 useEffect
中手动调用 fetch
(不推荐),或者依靠社区中流行的 React 库(例如 SWR 或 React 查询)进行客户端获取。
¥However, there are still cases where client-side data fetching makes sense. In these scenarios, you can manually call fetch
in a useEffect
(not recommended), or lean on popular React libraries in the community (such as SWR or React Query) for client fetching.
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
let res = await fetch('https://api.vercel.app/blog')
let 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>
)
}
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
let res = await fetch('https://api.vercel.app/blog')
let 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 或数据库缓存数据
¥Caching data with an ORM or Database
你可以使用 unstable_cache
API 来缓存响应,以允许在运行 next build
时预渲染页面。
¥You can use the unstable_cache
API to cache the response to allow pages to be prerendered when running next build
.
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>
)
}
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
,然后可以使用 增量静态再生 使其无效。
¥This example caches the result of the database query for 1 hour (3600 seconds). It also adds the cache tag posts
which can then be invalidated with Incremental Static Regeneration.
在多个函数中重用数据
¥Reusing data across multiple functions
Next.js 使用 generateMetadata
和 generateStaticParams
等 API,你需要使用在 page
中获取的相同数据。
¥Next.js uses APIs like generateMetadata
and generateStaticParams
where you will need to use the same data fetched in the page
.
如果你正在使用 fetch
,则可以通过添加 cache: 'force-cache'
将请求变为 memoized。这意味着你可以安全地使用相同的选项调用相同的 URL,并且只会发出一个请求。
¥If you are using fetch
, requests can be memoized by adding cache: 'force-cache'
. This means you can safely call the same URL with the same options, and only one request will be made.
很高兴知道:
¥Good to know:
在以前版本的 Next.js 中,使用
fetch
的默认cache
值为force-cache
。这在版本 15 中更改为默认值cache: no-store
。¥In previous versions of Next.js, using
fetch
would have a defaultcache
value offorce-cache
. This changed in version 15, to a default ofcache: no-store
.
import { notFound } from 'next/navigation'
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
let res = await fetch(`https://api.vercel.app/blog/${id}`, {
cache: 'force-cache',
})
let post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
let posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())
return posts.map((post: Post) => ({
id: post.id,
}))
}
export async function generateMetadata({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return {
title: post.title,
}
}
export default async function Page({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
import { notFound } from 'next/navigation'
async function getPost(id) {
let res = await fetch(`https://api.vercel.app/blog/${id}`)
let post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
let posts = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return posts.map((post) => ({
id: post.id,
}))
}
export async function generateMetadata({ params }) {
let post = await getPost(params.id)
return {
title: post.title,
}
}
export default async function Page({ params }) {
let post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
如果你不使用 fetch
,而是直接使用 ORM 或数据库,则可以使用 React cache
函数封装数据获取。这将删除重复项并仅进行一次查询。
¥If you are not using fetch
, and instead using an ORM or database directly, you can wrap your data fetch with the React cache
function. This will de-duplicate and only make one query.
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
})
重新验证缓存数据
¥Revalidating cached data
了解有关使用 增量静态再生 重新验证缓存数据的更多信息。
¥Learn more about revalidating cached data with Incremental Static Regeneration.
模式
¥Patterns
并行和顺序数据获取
¥Parallel and sequential data fetching
在组件内部获取数据时,你需要注意两种数据获取模式:并行和顺序。
¥When fetching data inside components, you need to be aware of two data fetching patterns: Parallel and Sequential.
-
顺序:组件树中的请求相互依赖。这可能会导致更长的加载时间。
¥Sequential: requests in a component tree are dependent on each other. This can lead to longer loading times.
-
并行:路由中的请求是预启动的,并将同时加载数据。这减少了加载数据所需的总时间。
¥Parallel: requests in a route are eagerly initiated and will load data at the same time. This reduces the total time it takes to load data.
顺序数据获取
¥Sequential data fetching
如果你有嵌套组件,并且每个组件都获取自己的数据,那么如果这些数据请求不是 memoized,则数据获取将按顺序进行。
¥If you have nested components, and each component fetches its own data, then data fetching will happen sequentially if those data requests are not memoized.
在某些情况下,你可能需要这种模式,因为一个提取取决于另一个提取的结果。例如,只有当 Artist
组件完成获取数据后,Playlists
组件才会开始获取数据,因为 Playlists
依赖于 artistID
属性:
¥There may be cases where you want this pattern because one fetch depends on the result of the other. For example, the Playlists
component will only start fetching data once the Artist
component has finished fetching data because Playlists
depends on the artistID
prop:
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// Get artist information
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<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>
)
}
export default async function Page({ params: { username } }) {
// Get artist information
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }) {
// 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 <Suspense>
(用于嵌套组件)在 React 结果流式传输时显示即时加载状态。
¥You can use loading.js
(for route segments) or React <Suspense>
(for nested components) to show an instant loading state while React streams in the result.
这将防止整个路由被数据请求阻塞,并且用户将能够与页面中已准备就绪的部分进行交互。
¥This will prevent the whole route from being blocked by data requests, and the user will be able to interact with the parts of the page that are ready.
并行数据获取
¥Parallel Data Fetching
默认情况下,布局和页面段是并行渲染的。这意味着请求将并行启动。
¥By default, layout and page segments are rendered in parallel. This means requests will be initiated in parallel.
但是,由于 async
/await
的性质,同一个段或组件内的等待请求将阻止其下方的任何请求。
¥However, due to the nature of async
/await
, an awaited request inside the same segment or component will block any requests below it.
要并行获取数据,你可以通过在使用数据的组件之外定义它们来预发起请求。通过并行启动两个请求可以节省时间,但是,在两个 promise 都得到解决之前,用户不会看到渲染的结果。
¥To fetch data in parallel, you can eagerly initiate requests by defining them outside the components that use the data. This saves time by initiating both requests in parallel, however, the user won't see the rendered result until both promises are resolved.
在下面的示例中,getArtist
和 getAlbums
函数在 Page
组件外部定义,并使用 Promise.all
在组件内部启动:
¥In the example below, the getArtist
and getAlbums
functions are defined outside the Page
component and initiated inside the component using Promise.all
:
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: { username },
}: {
params: { username: string }
}) {
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} />
</>
)
}
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(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 = getAlbums(username)
// Initiate both requests in parallel
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
此外,你可以添加 悬念边界 来分解渲染工作并尽快显示部分结果。
¥In addition, you can add a Suspense Boundary to break up the rendering work and show part of the result as soon as possible.
预加载数据
¥Preloading Data
防止瀑布的另一种方法是使用预加载模式,通过创建一个你在阻止请求之上预调用的实用程序函数。例如,checkIsAvailable()
阻止 <Item/>
渲染,因此你可以在它之前调用 preload()
以热切地启动 <Item/>
数据依赖。在渲染 <Item/>
时,其数据已被获取。
¥Another way to prevent waterfalls is to use the preload pattern by creating an utility function that you eagerly call above blocking requests. For example, checkIsAvailable()
blocks <Item/>
from rendering, so you can call preload()
before it to eagerly initiate <Item/>
data dependencies. By the time <Item/>
is rendered, its data has already been fetched.
请注意,preload
函数不会阻止 checkIsAvailable()
运行。
¥Note that preload
function doesn't block checkIsAvailable()
from running.
import { getItem } from '@/utils/get-item'
export const preload = (id: string) => {
// void evaluates the given expression and returns undefined
// https://web.nodejs.cn/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
import { getItem } from '@/utils/get-item'
export const preload = (id) => {
// void evaluates the given expression and returns undefined
// https://web.nodejs.cn/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export default async function Item({ id }) {
const result = await getItem(id)
// ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({ params: { id } }) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
很高兴知道:"preload" 函数也可以有任何名称,因为它是一个模式,而不是 API。
¥Good to know: The "preload" function can also have any name as it's a pattern, not an API.
使用带有预加载模式的 React cache
和 server-only
¥Using React cache
and server-only
with the Preload Pattern
你可以结合 cache
函数、preload
模式和 server-only
包来创建可在整个应用中使用的数据获取实用程序。
¥You can combine the cache
function, the preload
pattern, and the server-only
package to create a data fetching utility that can be used throughout your app.
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
import { cache } from 'react'
import 'server-only'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})
通过这种方法,你可以预获取数据,缓存响应,并保证此数据获取 只发生在服务器上。
¥With this approach, you can eagerly fetch data, cache responses, and guarantee that this data fetching only happens on the server.
布局、页面或其他组件可以使用 utils/get-item
导出来控制何时获取项目的数据。
¥The utils/get-item
exports can be used by Layouts, Pages, or other components to give them control over when an item's data is fetched.
很高兴知道:
¥Good to know:
我们建议使用
server-only
包 以确保服务器数据获取功能永远不会在客户端上使用。¥We recommend using the
server-only
package to make sure server data fetching functions are never used on the client.
防止敏感数据暴露给客户端
¥Preventing sensitive data from being exposed to the client
我们建议使用 React 的 taint API taintObjectReference
和 taintUniqueValue
,以防止整个对象实例或敏感值传递到客户端。
¥We recommend using React's taint APIs, taintObjectReference
and taintUniqueValue
, to prevent whole object instances or sensitive values from being passed to the client.
要在应用中启用污染,请将 Next.js Config experimental.taint
选项设置为 true
:
¥To enable tainting in your application, set the Next.js Config experimental.taint
option to true
:
module.exports = {
experimental: {
taint: true,
},
}
然后将要污染的对象或值传递给 experimental_taintObjectReference
或 experimental_taintUniqueValue
函数:
¥Then pass the object or value you want to taint to the experimental_taintObjectReference
or experimental_taintUniqueValue
functions:
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
}
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
}
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
/>
)
}
import { getUserData } from './data'
export async function Page() {
const userData = await getUserData()
return (
<ClientComponent
user={userData} // this will cause an error because of taintObjectReference
address={userData.address} // this will cause an error because of taintUniqueValue
/>
)
}