跳到主要内容

数据获取

数据获取、缓存和重新验证

数据获取是任何应用程序的核心部分。

本页介绍了在React和Next.js中如何获取、缓存和重新验证数据

四种获取数据的方式

  1. 在服务器上使用fetch
  2. 在服务器上使用第三方库
  3. 通过路由处理程序在客户端上获取
  4. 在客户端上使用第三方库。

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

Next.js扩展了原生的[fetch Web API允许您在服务器上为每个fetch请求配置 缓存和重新验证行为。 React通过在呈现React组件树时自动 记忆fetch请求来扩展fetch

您可以在Server Components、 Route HandlersServer Actions 中使用带有async/awaitfetch

app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// 返回值未序列化
// 可以返回Date、Map、Set等。

if (!res.ok) {
// 这将激活最接近的`error.js`错误边界
throw new Error('获取数据失败')
}

return res.json()
}

export default async function Page() {
const data = await getData()

return <main></main>
}
备注
  1. Next.js提供了在Server Components中获取数据时可能需要的有用函数,如cookiesheaders。 由于它们依赖于请求时间信息,它们将导致路由动态渲染。
  2. 在Route Handlers中,fetch请求不会被记忆,因为Route Handlers不是React组件树的一部分。
  3. 要在带有TypeScript的Server Component中使用async/await, 您需要使用TypeScript 5.1.3或更高版本和@types/react 18.2.8或更高版本。

缓存数据

缓存会存储数据,使得不必在每个请求上都重新从数据源获取数据。

默认情况下,Next.js会自动将fetch返回的值缓存到服务器上的 Data Cache中。 这意味着数据可以在构建时间或请求时间获取,被缓存,并在每次数据请求时重复使用。

// 'force-cache'是默认值,可以省略
fetch('https://...', { cache: 'force-cache' })

使用POST方法的fetch请求也会自动缓存。 除非它在使用POST方法的 Route Handler 中,否则它将不会被缓存。

什么是Data Cache?

Data Cache是一个持久性 HTTP缓存。 根据您的平台,缓存可以自动扩展并在 多个地区共享。 了解有关Data Cache的更多信息。

重新验证数据

重新验证是清除Data Cache并重新获取最新数据的过程。 当数据发生变化并且您希望确保显示最新信息时,这很有用。

缓存的数据可以通过两种方式重新验证

  • 基于时间的重新验证:在经过一定时间后自动重新验证数据。这对于数据变化不频繁且新鲜度不是很关键的数据很有用。
  • 按需重新验证:根据事件(例如表单提交)手动重新验证数据。按需重新验证可以使用基于标签或基于路径的方法一次重新验证多个数据组。当您希望尽快显示最新数据时很有用(例如,当来自您的无头CMS的内容已更新时)。

基于时间的重新验证

要在定时间隔内重新验证数据,可以使用fetchnext.revalidate选项设置资源的缓存生存时间(以秒为单位)。

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

或者,要重新验证路由段中的所有fetch请求,可以使用 Segment Config Options

layout.js | page.js
export const revalidate = 3600 // 最多每小时重新验证一次

如果在静态渲染的路由中有多个fetch请求, 且每个请求具有不同的重新验证频率, 则将使用最低时间来重新验证所有请求。 对于动态渲染的路由,每个fetch请求将独立重新验证。

了解有关基于时间的重新验证的更多信息。

按需重新验证

可以通过路径(revalidatePath)或缓存标签(revalidateTag)在 Server ActionRoute Handler 中重新验证数据。

Next.js具有用于在跨路由无效化fetch请求的缓存标记系统。

  1. 在使用fetch时,您可以选择使用一个或多个标签标记缓存条目。
  2. 然后,您可以调用revalidateTag以重新验证与该标签关联的所有条目。

例如,以下fetch请求添加了缓存标签collection

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

然后,您可以在Server Action中调用revalidateTag来重新验证带有collection标签的此fetch调用:

app/actions.ts
'use server'

import { revalidateTag } from 'next/cache'

export default async function action() {
revalidateTag('collection')
}

了解有关按需重新验证的更多信息。

错误处理和重新验证

如果在尝试重新验证数据时抛出错误,将继续从缓存中提供上次成功生成的数据。 在下一次请求时,Next.js将重试重新验证数据。

退出数据缓存

如果存在以下情况,则不会fetch请求进行缓存:

  1. cache: 'no-store'添加到fetch请求中。
  2. revalidate: 0选项添加到个别fetch请求。
  3. fetch请求在使用POST方法的Router Handler中。
  4. fetch请求在headerscookies的使用之后。
  5. 使用const dynamic = 'force-dynamic'路由段选项。
  6. 通过配置fetchCache路由段选项跳过缓存,默认情况下不会缓存。
  7. fetch请求使用AuthorizationCookie头,并且在组件树中有未缓存的请求。

个别fetch请求

要禁用个别fetch请求的缓存,可以在fetch的选项中

将cache选项设置为'no-store'。这将在每个请求上动态获取数据。

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

fetch API参考 中查看所有可用的缓存选项。

多个fetch请求

如果在一个路由段(例如LayoutPage)中有多个fetch请求, 可以使用 Segment Config Options 配置段中所有数据请求的缓存行为。

但是,我们建议单独配置每个fetch请求的缓存行为。这样可以更精细地控制缓存行为。

在服务器上使用第三方库获取数据

在使用不支持或公开fetch的第三方库的情况下(例如数据库、CMS或ORM客户端), 可以使用Route Segment Config Option 和React的cache函数配置这些请求的缓存和重新验证行为。

数据是否被缓存将取决于路由段是 静态还是动态渲染。 如果段是静态的(默认值),则请求的输出将被缓存并作为路由段的一部分重新验证。 如果段是动态的,则请求的输出将不会被缓存,并且将在渲染段时的每个请求上重新获取。

还可以使用实验性的unstable_cache API

示例

在下面的示例中:

  • 使用React cache函数来记忆数据请求。
  • 在Layout和Page段中,将revalidate选项设置为3600,这意味着数据将每小时最多缓存和重新验证一次。
app/utils.ts
import { cache } from 'react'

export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id })
return item
})

尽管getItem函数调用了两次,但数据库只会进行一次查询。

app/item/[id]/layout.tsx
import { getItem } from '@/utils/get-item'

export const revalidate = 3600 // 每小时最多重新验证一次数据

export default async function Layout({
params: { id },
}: {
params: { id: string }
}) {
const item = await getItem(id)
// ...
}
app/item/[id]/page.tsx
import { getItem } from '@/utils/get-item'

export const revalidate = 3600 // 每小时最多重新验证一次数据

export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
const item = await getItem(id)
// ...
}

在客户端使用Route Handlers获取数据

如果需要在客户端组件中获取数据,可以从客户端调用 Route Handler。 Route Handlers在服务器上执行并将数据返回给客户端。当您不希望将敏感信息暴露给客户端时,例如API令牌时,这很有用。

请参阅Route Handler文档以获取示例。

Server Components和Route Handlers

由于Server Components在服务器上呈现,因此不需要从Server Component中调用Route Handler来获取数据。 相反,您可以直接在Server Component内部获取数据。

在客户端使用第三方库获取数据

您还可以使用第三方库(如SWRReact Query)在客户端获取数据。 这些库提供了它们自己的API,用于记忆请求、缓存、重新验证和变异数据。

未来的API:

use是一个接受并处理由函数返回的Promise的React函数。 当前不建议在Client Components中将fetch包装在use中,因为这可能触发多次重新渲染。 在React文档中了解有关use的更多信息。

Server Actions and Mutations

Server Actions是在服务器上执行的异步函数。 它们可以在Next.js应用程序的Server和Client Components中用于处理表单提交和数据变更。

🎥 观看:通过Server Actions学习有关表单和变更的更多信息 → YouTube(10分钟)

约定

可以使用React的"use server"指令定义Server Action。 您可以将该指令放在异步函数的顶部,以将该函数标记为Server Action, 或者将其放在单独的文件的顶部,以将该文件的所有导出标记为Server Action。

Server Components

Server Components可以使用内联函数级别或模块级别的"use server"指令。 要内联Server Action,请在函数体的顶部添加"use server"

app/page.tsx
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'

// ...
}

return (
// ...
)
}

Client Components

Client Components只能导入使用模块级别"use server"指令的操作。

要在Client Component中调用Server Action,请创建一个新文件,并在其顶部添加"use server"指令。 文件中的所有函数都将被标记为Server Actions,可以在Client和Server Components中重复使用:

app/actions.ts
'use server'

export async function create() {
// ...
}
app/ui/button.tsx
import { create } from '@/app/actions'

export function Button() {
return (
// ...
)
}

您还可以将Server Action作为prop传递给Client Component:

<ClientComponent updateItem={updateItem} />
app/client-component.jsx
'use client'

export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}

行为

  • Server actions可以使用<form>元素的action属性调用:
    • Server Components默认支持渐进增强,这意味着即使JavaScript尚未加载或被禁用,表单也将被提交。
    • 在Client Components中,如果JavaScript尚未加载,调用Server Actions的表单将排队提交,优先考虑客户端水合。
    • 在水合后,浏览器不会在表单提交时刷新。
  • Server Actions不仅限于<form>,还可以从事件处理程序、useEffect、第三方库和其他表单元素(如<button>)中调用。
  • Server Actions与Next.js的缓存和重新验证架构集成。 当调用一个动作时,Next.js可以在单个服务器往返中返回更新后的UI和新数据。
  • 在幕后,动作使用POST方法,只有此HTTP方法才能调用它们。
  • Server Actions的参数和返回值必须由React可序列化。请参阅React文档以获取 可序列化参数和值的列表。
  • Server Actions是函数。这意味着它们可以在应用程序的任何地方重复使用。
  • Server Actions继承它们所用页面或布局的 运行时

示例

表单

React扩展了HTML的<form>元素,以允许使用action prop调用Server Actions。

在表单中调用时,action会自动接收 FormData对象。 您无需使用React的useState来管理字段,而是可以使用本机的 FormData方法提取数据:

app/invoices/page.tsx
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'

const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}

// mutate data
// revalidate cache
}

return <form action={createInvoice}>...</form>
}

需要注意的是:

  • 示例:带有加载和错误状态的表单
  • 在处理具有许多字段的表单时,您可能希望考虑使用JavaScript的Object.fromEntries()方法。 例如:const rawFormData = Object.fromEntries(formData.entries())
  • 有关更多信息,请参阅React <form>文档。

传递额外参数

您可以使用JavaScript的bind方法将额外参数传递给Server Action。

app/client-component.tsx
'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)

return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}

Server Action将接收userId参数,除了表单数据:

app/actions.js
'use server'

export async function updateUser(userId, formData) {
// ...
}

需要注意的是:

  • 作为替代方案,可以在表单中将参数作为隐藏的输入字段传递(例如<input type="hidden" name="userId" value={userId} />)。 但是,该值将成为渲染的HTML的一部分,不会被编码。
  • .bind在Server和Client Components中都可以工作。它还支持渐进增强。

待定状态

您可以使用React的useFormStatus hook在表单提交时显示待定状态。

  • useFormStatus为特定的<form>返回状态,因此必须将其定义为<form>元素的子元素
  • useFormStatus是一个React hook,因此必须在Client Component中使用。
app/submit-button.tsx
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
const { pending } = useFormStatus()

return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}

<SubmitButton />然后可以嵌套在任何表单中:

app/page.tsx
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'

// Server Component
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}

服务器端验证和错误处理

我们建议使用HTML验证,如requiredtype="email"进行基本的客户端表单验证。

对于更高级的服务器端验证,您可以使用 zod 等库在变异数据之前验证表单字段:

app/actions.ts
'use server'

import { z } from 'zod'

const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})

export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})

// 如果表单数据无效,请提前返回
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}

// Mutate data
}

一旦在服务器上验证了字段,您就可以在操作中返回可序列化的对象, 并使用React的useFormState hook来向用户显示消息。

  • 通过将操作传递给useFormState,操作的函数签名会更改, 以接收新的prevStateinitialState参数作为其第一个参数。
  • useFormState 是一个 React hook,因此必须在客户端组件中使用。
app/actions.ts
'use server'

export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Please enter a valid email',
}
}

然后,您可以将操作传递给useFormState hook,并使用返回的状态来显示错误消息。

app/ui/signup.tsx
'use client'

import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'

const initialState = {
message: null,
}

export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)

return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Sign up</button>
</form>
)
}

需要注意的是:

  • 在变异数据之前,您应始终确保用户还被授权执行该操作。请参阅 身份验证和授权

乐观更新

您可以使用React的useOptimistic hook在Server Action完成之前乐观地更新UI,而不是等待响应:

app/page.tsx
'use client'

import { useOptimistic } from 'react'
import { send } from './actions'

type Message = {
message: string
}

export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
messages,
(state: Message[], newMessage: string) => [
...state,
{ message: newMessage },
]
)

return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData: FormData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}

嵌套元素

您可以在嵌套在<form>内的元素中调用Server Action, 例如<button><input type="submit"><input type="image">。 这些元素接受formAction prop或事件处理程序

这在您想要在一个表单中调用多个服务器操作的情况下很有用。 例如,您可以为保存文章草稿和发布文章分别创建一个特定的<button>元素。 有关更多信息,请参阅React <form>文档

非表单元素

虽然在<form>元素中使用Server Actions很常见,但它们也可以从代码的其他部分调用,例如事件处理程序和useEffect

事件处理程序

您可以从onClick等事件处理程序中调用Server Action,例如增加喜欢次数:

app/like-button.tsx
'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)

return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}

为了提高用户体验,我们建议使用其他React API, 如useOptimisticuseTransition, 在服务器操作在服务器上执行之前更新UI,或者在展示待定状态时。

您还可以向表单元素添加事件处理程序,例如在onChange时保存表单字段:

app/ui/edit-post.tsx
'use client'

import { publishPost, saveDraft } from './actions'

export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publish</button>
</form>
)
}

对于像这样可能快速触发多个事件的情况,我们建议使用防抖以防止不必要的Server Action调用。

useEffect

您可以使用React的useEffect hook在组件挂载时或依赖项更改时调用Server Action。 这对于依赖于全局事件或需要自动触发的变异很有用。 例如,用于应用程序快捷键的onKeyDown,用于无限滚动的交叉观察器hook,或在组件挂载时更新查看计数:

app/view-count.tsx
'use client'

import { incrementViews } from './actions'
import { useState, useEffect } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)

useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}

updateViews()
}, [])

return <p>Total Views: {views}</p>
}

请记住考虑 useEffect行为和注意事项

错误处理

当发生错误时,它将被客户端上最近的error.js<Suspense>边界捕获。 我们建议使用try/catch将错误返回以由您的UI处理。

例如,您的Server Action可能会通过返回消息来处理创建新项目时的错误:

app/actions.ts
'use server'

export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}

值得知道:

  • 除了抛出错误之外,您还可以返回一个对象,由useFormStatus处理。请参阅服务器端验证和错误处理。

重新验证数据

您可以在Server Actions中使用revalidatePath API重新验证 Next.js缓存

app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
try {
// ...
} catch (error) {
// ...
}

revalidatePath('/posts')
}

或者使用revalidateTag使用缓存标签使特定数据获取失效:

app/actions.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
try {
// ...
} catch (error) {
// ...
}

revalidateTag('posts')
}

重定向

如果希望在完成Server Action后将用户重定向到不同的路由, 则可以使用redirect API。redirect需要在try/catch块之外调用:

app/actions.ts
'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}

revalidateTag('posts') // 更新缓存的文章
redirect(`/post/${id}`) // 转到新的文章页面
}

Cookies

您可以在Server Action中使用cookies API的getsetdeleteCookie:

app/actions.ts
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
// 获取Cookie
const value = cookies().get('name')?.value

// 设置Cookie
cookies().set('name', 'Delba')

// 删除Cookie
cookies().delete('name')
}

请查看从Server Actions删除Cookie的 其他示例

安全性

身份验证和授权

您应该将Server Actions视为面向公共API端点的方式,并确保用户被授权执行操作。例如:

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}

// ...
}

闭包和加密

在组件内定义Server Action会创建一个闭包, 其中该动作可以访问外部函数范围的变量。 例如,publish动作可以访问publishVersion变量:

app/page.tsx
export default function Page() {
const publishVersion = await getLatestVersion();

async function publish(formData: FormData) {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}

return <button action={publish}>Publish</button>;
}

当您需要捕获数据快照(例如publishVersion)以便在调用动作时稍后使用时,闭包很有用。

然而,为了发生这种情况,捕获的变量在动作被调用时被发送到客户端,并在动作被调用时返回服务器。 为防止将敏感数据暴露给客户端,Next.js会自动加密封闭的变量。 每次构建Next.js应用程序时,都会为每个动作生成一个新的私钥。这意味着动作只能为特定构建调用。

备注

我们不建议仅依赖加密防止将敏感值暴露给客户端。 相反,您应该使用React污染API 主动阻止将特定数据发送到客户端。

覆盖加密密钥(高级)

在自托管Next.js应用程序跨多个服务器部署时,每个服务器实例可能会使用不同的加密密钥,导致潜在的不一致性。

为减轻这种情况,您可以使用process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY环境变量覆盖加密密钥。 指定此变量确保您的加密密钥在构建之间保持一致,并且所有服务器实例都使用相同的密钥。

这是一个需要在应用程序中实现一致的加密行为的高级用例。您应该考虑采用标准的安全实践,如密钥轮换和签名。

值得知道:部署到Vercel的Next.js应用程序会自动处理此问题。

允许的来源(高级)

由于Server Actions可以在<form>元素中调用,这使它们容易受到CSRF攻击的威胁。

在幕后,Server Actions使用POST方法,只有此HTTP方法才能调用它们。 这在现代浏览器中防止了大多数CSRF漏洞, 特别是由于SameSite cookies成为默认设置。

作为额外的保护,Next.js中的Server Actions还将Origin头Host头(或X-Forwarded-Host)进行比较。 如果这些不匹配,请求将被中止。换句话说,Server Actions只能在托管它的页面的同一主机上调用。

对于使用反向代理或多层后端体系结构(其中服务器API与生产域不同)的大型应用程序, 建议使用serverActions.allowedOrigins选项来指定一组安全的来源。该选项接受一个字符串数组。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}

了解有关安全性和Server Actions的更多信息。

其他资源

有关Server Actions的更多信息,请查阅以下React文档:

模式和最佳实践

在React和Next.js中获取数据有一些推荐的模式和最佳实践。本页面将介绍一些最常见的模式以及如何使用它们。

在服务器上获取数据

在可能的情况下,我们建议使用Server Components在服务器上获取数据。这使您能够:

  • 直接访问后端数据资源(例如数据库)。
  • 通过阻止敏感信息(例如访问令牌和API密钥)暴露给客户端,从而使应用程序更加安全。
  • 在相同的环境中获取数据并进行渲染。这减少了客户端和服务器之间的往返通信,以及客户端上主线程的工作
  • 使用单个往返而不是在客户端上发起多个单独的请求来执行多个数据获取。
  • 减少客户端服务器瀑布效应
  • 根据您的区域,数据获取还可以更接近您的数据源,减少延迟并提高性能。

然后,您可以使用Server Actions 对数据进行突变或更新。

在需要时获取数据

如果需要在树中的多个组件中使用相同的数据(例如当前用户),则无需全局获取数据,也无需在组件之间传递props。 相反,您可以在需要数据的组件中使用fetch或Reactcache,而无需担心为相同数据发出多个请求的性能影响。

这是可能的,因为fetch请求会自动进行记忆。 了解有关请求记忆的更多信息

备注

这也适用于布局,因为不可能在父布局和其子布局之间传递数据。

流式处理

流式处理和Suspense是React的特性, 它们允许您逐渐呈现和逐步将UI的渲染单元流式传输到客户端。

通过Server Components和嵌套布局, 您可以立即呈现不需要特定数据的页面部分, 并为获取数据的页面部分显示加载状态。 这意味着用户无需等待整个页面加载完毕,就可以开始与之交互。

server rendering with streaming

要了解有关流式处理和Suspense的更多信息,请参阅 Loading UIStreaming and Suspense页面。

并行和顺序数据获取

在React组件内获取数据时,需要了解两种数据获取模式顺序并行

sequential parallel data fetching

  • 使用顺序数据获取时,路由中的请求彼此依赖,因此创建瀑布效应。可能有情况下您希望使用这种模式,因为一个获取取决于另一个的结果,或者您希望在下一个获取之前满足某个条件以节省资源。然而,此行为也可能是无意的,导致较长的加载时间。
  • 使用并行数据获取时,路由中的请求会被急切地初始化,并且将同时加载数据。这减少了客户端服务器瀑布效应以及加载数据所需的总时间。

顺序数据获取

如果有嵌套组件,并且每个组件都获取自己的数据,那么如果这些数据请求不同, 那么数据获取将按顺序进行(对于请求相同的数据不适用, 因为它们会自动进行记忆)。

例如,Playlists组件只有在Artist组件完成数据获取后才会开始获取数据, 因为Playlists依赖于artistID prop:

app/artist/[username]/page.tsx
// ...
async function Playlists({ artistID }: { artistID: string }) {
// 等待播放列表
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 },
}: {
params: { username: string }
}) {
// 等待艺术家
const artist = await getArtist(username)

return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}

在这种情况下,您可以使用loading.js(对于路由片段)或 React <Suspense> (对于嵌套组件)显示即时加载状态,同时React将结果流式传输。

这将阻止整个路由因数据获取而被阻塞,用户将能够与未被阻塞的页面部分进行交互。

阻止数据请求:

防止瀑布的另一种方法是在应用程序的根部全局获取数据,但这将阻止所有位于其下的路由片段的呈现,直到数据加载完成。 这可以描述为“一切或无”。要么你有整个页面或应用程序的所有数据,要么没有。

任何带有await的获取请求都将阻止渲染和数据获取其下的整个树,除非它们被包装在<Suspense>边界中或使用了loading.js。 另一种选择是使用并行数据获取或预加载模式。

并行数据获取

要并行获取数据,可以通过在使用数据的组件之外定义请求,然后从组件内部调用它们来急切地启动请求。 这样做可以通过同时初始化两个请求来节省时间,但是在两个promise都解决之前,用户将无法看到渲染的结果。

在下面的示例中,getArtistgetArtistAlbums函数在Page组件之外定义, 然后在组件内调用,我们等待两个promise解决:

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 getArtistAlbums(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 = getArtistAlbums(username)

// 等待promise解决
const [artist, albums] = await Promise.all([artistData, albumsData])

return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}

为了改善用户体验,可以添加Suspense边界 来分隔渲染工作,并尽快显示部分结果。

预加载数据

防止瀑布的另一种方法是使用预加载模式。您可以选择创建一个preload函数以进一步优化并行数据获取。 通过这种方式,您不必将promises作为props传递。preload函数的名称可以是任何名称,因为它是一种模式,而不是API。

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

export const preload = (id: string) => {
// void评估给定的表达式并返回undefined
// https://developer.mozilla.org/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: { id },
}: {
params: { id: string }
}) {
// 开始加载项目数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()

return isAvailable ? <Item id={id} /> : null
}

使用Reactcacheserver-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的污染APIs, taintObjectReferencetaintUniqueValue, 以防止整个对象实例或敏感值被传递到客户端。

要在应用程序中启用污染,请将Next.js Config的experimental.taint选项设置为true

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

然后将要污染的对象或值传递给experimental_taintObjectReferenceexperimental_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 phone number to the client",
data,
data.phoneNumber
)
return data
}
app/page.tsx
import { getUserData } from './data'

export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // 这将由于taintObjectReference而导致错误
phoneNumber={userData.phoneNumber} // 这将由于taintUniqueValue而导致错误
/>
)
}

了解更多关于安全性和服务器操作的信息。