数据获取
数据获取是任何应用的核心部分。本页介绍了使用你喜欢的方法获取数据的最佳实践。
¥Data fetching is a core part of any application. This page walks through best practices for fetching data using your preferred method.
我应该在服务器还是客户端获取数据?
¥Should I fetch data on the server or the client?
决定是在服务器还是客户端上获取数据取决于你正在构建的 UI 类型。
¥Deciding whether to fetch data on the server or the client depends on the type of UI you're building.
对于大多数不需要实时数据(例如轮询)的情况,你可以使用 服务器组件 在服务器上获取数据。这样做有几个好处:
¥For most cases, where you don't need real-time data (e.g. polling), you can fetch data on the server with Server Components. There are a few benefits to doing this:
-
你可以在一次服务器往返中获取数据,从而减少网络请求和客户端-服务器瀑布的数量。
¥You can fetch data in a single server-round trip, reducing the number of network requests and client-server waterfalls.
-
防止敏感信息(例如访问令牌和 API 密钥)暴露给客户端(这需要中间 API 路由)。
¥Prevent sensitive information, such as access tokens and API keys, from being exposed to the client (which would require an intermediate API route).
-
通过获取靠近源的数据来减少延迟(如果你的应用代码和数据库位于同一区域)。
¥Reduce latency by fetching data close to the source (if your application code and database are in the same region).
-
数据请求可以是 cached 和 revalidated。
¥Data requests can be cached and revalidated.
但是,服务器端数据提取将导致整个页面在服务器上重新渲染。如果你需要改变/重新验证较小的 UI 片段或持续获取实时数据(例如实时视图),客户端数据获取可能更适合,因为它允许你在客户端上重新渲染特定的 UI 片段。
¥However, server-side data fetching will cause the whole page to be re-rendered on the server. In cases where you need to mutate/revalidate smaller pieces of UI or continually fetch real-time data (e.g. live views), client-side data fetching might be better suited, as it'll allow you to re-render the specific piece of UI on the client.
在 Next.js 中有 4 种方法可以获取数据:
¥There are 4 ways you can fetch data in Next.js:
-
服务器上的
fetch
API。¥
fetch
API on the server. -
服务器上的 ORM 或数据库客户端。
¥ORMs or Database Clients on the server.
-
服务器上的 路由处理程序,通过客户端。
¥Route Handlers on the server, via the client.
-
客户端上的 数据获取库。
¥Data Fetching Libraries on the client.
fetch
API
Next.js 扩展了原生 fetch
网络应用接口 fetch
,允许你为服务器上的每个获取请求配置 caching 和 revalidating 行为。你可以在 服务器组件、路由处理程序 和 服务器操作 中使用 fetch
。例如:
¥Next.js extends the native fetch
Web API fetch
to allow you to configure the caching and revalidating behavior for each fetch request on the server. You can use fetch
in Server Components, Route Handlers, and Server Actions. For example:
export default async function Page() {
const data = await fetch('https://api.example.com/...').then((res) =>
res.json()
)
return '...'
}
export default async function Page() {
const data = await fetch('https://api.example.com/...').then((res) =>
res.json()
)
return '...'
}
默认情况下,fetch
请求会检索新数据。使用它将导致整个路由为 动态渲染,并且数据不会被缓存。
¥By default, fetch
requests retrieve fresh data. Using it will cause a whole route to be dynamically rendered and data will not be cached.
你可以通过将 cache
选项设置为 force-cache
来缓存 fetch
请求。这意味着数据将被缓存,并且组件将是 静态渲染:
¥You can cache fetch
requests by setting the cache
option to force-cache
. This means data will be cached, and the component will be statically rendered:
fetch('https://...', { cache: 'force-cache' })
或者,如果使用 PPR,我们建议将使用 fetch
请求的组件封装在 Suspense 边界中。这将确保只有使用 fetch
的组件被动态渲染和流式传输,而不是整个页面:
¥Alternatively, if using PPR, we recommend wrapping components that use fetch
requests in Suspense boundaries. This will ensure only the component that uses fetch
is dynamically rendered and streamed in, rather than the entire page:
import { Suspense } from 'react'
export default async function Cart() {
const res = await fetch('https://api.example.com/...')
return '...'
}
export default function Navigation() {
return (
<>
<Suspense fallback={<LoadingIcon />}>
<Cart />
</Suspense>
<>
)
}
有关更多信息,请参阅 缓存和重新验证 文档。
¥See the caching and revalidating docs for more information.
很高兴知道:在 Next.js 14 及更早版本中,
fetch
请求默认被缓存。请参阅 升级指南 了解更多信息。¥Good to know: In Next.js 14 and earlier,
fetch
requests were cached by default. See the upgrade guide for more information.
请求记忆
¥Request Memoization
如果你需要在树中的多个组件中获取相同的数据,则不必全局获取数据并向下传递 props。相反,你可以在需要数据的组件中获取数据,而不必担心对同一数据发出多个请求对性能的影响。
¥If you need to fetch the same data in multiple components in a tree, you do not have to fetch data globally and pass props down. Instead, you can fetch data in the components that need the data without worrying about the performance implications of making multiple requests for the same data.
这是可能的,因为具有相同 URL 和选项的 fetch
请求会在 React 渲染过程中自动记住。
¥This is possible because fetch
requests with the same URL and options are automatically memoized during a React render pass.
了解有关 请求记忆 的更多信息。
¥Learn more about request memoization.
ORM 和数据库客户端
¥ORMs and Database Clients
你可以在 服务器组件、路由处理程序 和 服务器操作 中调用你的 ORM 或数据库客户端。
¥You can call your ORM or database client in Server Components, Route Handlers, and Server Actions.
你可以在 React 渲染过程中使用 React cache
来记忆数据请求。例如,尽管在布局和页面中都调用了 getItem
函数,但只会对数据库进行一次查询:
¥You can use React cache
to memoize data requests during a React render pass. For example, although the getItem
function is called in both layout and page, only one query will be made to the database:
import { cache } from 'react'
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id })
return item
})
import { cache } from 'react'
export const getItem = cache(async (id) => {
const item = await db.item.findUnique({ id })
return item
})
import { getItem } from '@/utils/get-item'
export default async function Layout({
params: { id },
}: {
params: { id: string }
}) {
const item = await getItem(id)
// ...
}
import { getItem } from '@/utils/get-item'
export default async function Layout({ params: { id } }) {
const item = await getItem(id)
// ...
}
import { getItem } from '@/utils/get-item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
const item = await getItem(id)
// ...
}
import { getItem } from '@/utils/get-item'
export default async function Page({ params: { id } }) {
const item = await getItem(id)
// ...
}
你还可以使用实验性的 unstable_cache
和 unstable_noStore
API 配置这些请求的缓存和重新验证行为。
¥You can also configure the caching and revalidating behavior of these requests using the experimental unstable_cache
and unstable_noStore
APIs.
数据获取库
¥Data Fetching Libraries
你可以使用数据获取库(例如 SWR 或 React 查询)在客户端组件中获取数据。这些库提供自己的 API 来缓存、重新验证和改变数据。
¥You can fetch data in Client Components using data fetching libraries such as SWR or React Query. These libraries provide their own APIs for caching, revalidating, and mutating data.
例如,使用 SWR 在客户端上定期获取数据:
¥For example, using SWR to fetch data periodically on the client:
"use client"
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
export default function PollingComponent {
// Polling interval set to 2000 milliseconds
const { data } = useSWR('/api/data', fetcher, { refreshInterval: 2000 });
return '...'
}
"use client"
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
export default function PollingComponent {
// Polling interval set to 2000 milliseconds
const { data } = useSWR('/api/data', fetcher, { refreshInterval: 2000 });
return '...'
}
路由处理程序
¥Route Handlers
如果你需要创建 API 端点,Next.js 支持 路由处理程序。路由处理程序在服务器上执行并防止敏感信息暴露给客户端(例如 API 凭据)。
¥If you need to create API endpoints, Next.js supports Route Handlers. Route Handlers execute on the server and prevent sensitive information from being exposed to the client (e.g. API credentials).
例如,使用 SWR 调用路由处理程序:
¥For example, using SWR to call a Route Handler:
'use client'
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
export default function Message() {
const { data } = useSWR('/api/messages', fetcher)
return '...'
}
'use client'
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
export default function Message() {
const { data } = useSWR('/api/messages', fetcher)
return '...'
}
export async function GET() {
const data = await fetch('https://...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
}).then((res) => res.json())
return Response.json({ data })
}
export async function GET() {
const data = await fetch('https://...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
}).then((res) => res.json())
return Response.json({ data })
}
有关更多示例,请参阅 路由处理程序 文档。
¥See the Route Handler docs for more examples.
很高兴知道:由于服务器组件在服务器上渲染,因此你无需从服务器组件调用路由处理程序。你可以直接访问你的数据。
¥Good to know: Since Server Components render on the server, you don't need to call a Route Handler from a Server Component. You can access your data directly.
模式
¥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>
{/* 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>
)
}
export default async function Page({ params: { username } }) {
// 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 }) {
// 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
/>
)
}