模式与最佳实践

在 React 和 Next.js 中有一些推荐的数据获取模式和最佳实践。本页将介绍一些最常见的模式及其使用方法。

在服务端获取数据

我们建议尽可能使用服务端组件 (Server Components) 在服务端获取数据。这样做可以:

  • 直接访问后端数据资源(如数据库)
  • 通过防止敏感信息(如访问令牌和 API 密钥)暴露给客户端,提高应用安全性
  • 在同一环境中完成数据获取和渲染,减少客户端与服务端之间的往返通信,同时减轻客户端的主线程工作负担
  • 通过单次往返完成多次数据获取,而非在客户端发起多个独立请求
  • 减少客户端-服务端的级联请求
  • 根据所在地区,数据获取可以更靠近数据源,从而降低延迟并提升性能

之后,您可以使用服务端操作 (Server Actions) 来变更或更新数据。

在需要的地方获取数据

如果需要在组件树中的多个组件中使用相同数据(如当前用户),您无需全局获取数据,也不需要在组件间传递 props。相反,您可以在需要数据的组件中使用 fetch 或 React 的 cache,而无需担心因重复请求相同数据而影响性能。

这是因为 fetch 请求会自动进行记忆化 (memoized)。了解更多关于请求记忆化的内容。

须知:这也适用于布局 (layouts),因为无法在父布局与其子组件之间传递数据。

流式渲染 (Streaming)

流式渲染和 Suspense 是 React 的功能,允许您逐步渲染并将 UI 单元增量式地流式传输到客户端。

借助服务端组件和嵌套布局,您可以立即渲染不需要特定数据的页面部分,并为正在获取数据的页面部分显示加载状态。这意味着用户无需等待整个页面加载完成即可开始交互。

服务端流式渲染

要了解更多关于流式渲染和 Suspense 的内容,请参阅加载 UI流式渲染与 Suspense 页面。

并行与顺序数据获取

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

顺序与并行数据获取
  • 顺序数据获取:路由中的请求相互依赖,因此会产生级联请求。某些情况下您可能需要这种模式,因为某个请求依赖于另一个请求的结果,或者您希望在满足条件后再进行下一个请求以节省资源。但有时这种行为可能是无意的,会导致加载时间延长。
  • 并行数据获取:路由中的请求会立即发起,同时加载数据。这可以减少客户端-服务端的级联请求,缩短数据加载的总时间。

顺序数据获取

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

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

// ...

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>加载中...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

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

这将防止整个路由被数据获取阻塞,用户可以与被阻塞部分之外的页面内容进行交互。

阻塞式数据请求:

防止级联请求的另一种方法是在应用的根节点全局获取数据,但这会阻塞其下所有路由段的渲染,直到数据加载完成。这可以称为"全有或全无"的数据获取方式。要么获取整个页面或应用的所有数据,要么不获取任何数据。

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

并行数据获取

要在并行中获取数据,可以在使用数据的组件外部定义请求,然后在组件内部调用它们。这样可以同时发起两个请求以节省时间,但用户需要等待两个 Promise 都解析后才能看到渲染结果。

在下面的示例中,getArtistgetArtistAlbums 函数在 Page 组件外部定义,然后在组件内部调用,并等待两个 Promise 解析:

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 函数来进一步优化并行数据获取。使用这种方法,您无需将 Promise 作为 props 传递。preload 函数可以有任何名称,因为它是一种模式,而非 API。

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

结合使用 React cacheserver-only 和预加载模式

您可以结合使用 cache 函数、预加载模式和 server-only 包,创建一个可在整个应用中使用的数据获取工具。

import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})

通过这种方法,您可以预先获取数据、缓存响应,并确保数据获取仅在服务端进行

布局 (Layouts)、页面 (Pages) 或其他组件可以使用 utils/get-item 导出的功能来控制何时获取项目数据。

须知:

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

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

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

要在应用中启用污染功能,将 Next.js 配置中的 experimental.taint 选项设置为 true

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

然后将要污染的对象或值传递给 experimental_taintObjectReferenceexperimental_taintUniqueValue 函数:

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    '不要将整个用户对象传递给客户端',
    data
  )
  experimental_taintUniqueValue(
    "不要将用户的地址传递给客户端",
    data,
    data.address
  )
  return data
}

了解更多关于安全与服务端操作的内容。

On this page