Menu

数据获取

数据获取是任何应用程序的核心部分。本页将介绍使用你喜欢的方法获取数据的最佳实践。

我应该在服务器还是客户端获取数据?

决定是在服务器还是客户端获取数据取决于你正在构建的 UI 类型。

对于大多数情况,当你不需要实时数据 (例如轮询) 时,你可以使用 服务器组件 在服务器上获取数据。这样做有几个好处:

  • 你可以在单次服务器往返中获取数据,减少网络请求数量和客户端-服务器瀑布。
  • 防止敏感信息 (如访问令牌和 API 密钥) 暴露给客户端 (这将需要中间 API 路由)。
  • 通过在靠近数据源的地方获取数据来减少延迟 (如果你的应用程序代码和数据库在同一区域)。
  • 数据请求可以被 缓存重新验证

然而,服务器端数据获取将导致整个页面在服务器上重新渲染。在需要变更/重新验证较小的 UI 片段或持续获取实时数据 (例如实时视图) 的情况下,客户端数据获取可能更合适,因为它允许你在客户端重新渲染特定的 UI 片段。

在 Next.js 中有 4 种获取数据的方式:

fetch API

Next.js 扩展了原生的 fetch Web API,允许你为服务器上的每个获取请求配置 缓存重新验证 行为。你可以在 服务器组件路由处理程序服务器操作 中使用 fetch。例如:

app/page.tsx
export default async function Page() {
  const data = await fetch("https://api.example.com/...").then((res) =>
    res.json()
  );
 
  return "...";
}
app/page.js
export default async function Page() {
  const data = await fetch("https://api.example.com/...").then((res) =>
    res.json()
  );
 
  return "...";
}

默认情况下,fetch 请求会获取新数据。使用它将导致整个路由被 动态渲染,并且数据不会被缓存。

你可以通过设置 cache 选项为 force-cache 来缓存 fetch 请求。这意味着数据将被缓存,并且组件将被 静态渲染

fetch("https://...", { cache: "force-cache" });

或者,如果使用 PPR,我们建议将使用 fetch 请求的组件包装在 Suspense 边界中。这将确保只有使用 fetch 的组件被动态渲染和流式传输,而不是整个页面:

@/app/ui/cart.tsx
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>
    <>
  )
}

有关更多信息,请参阅 缓存和重新验证 文档。

值得注意的是:在 Next.js 14 及更早版本中,fetch 请求默认被缓存。有关更多信息,请参阅 升级指南

请求记忆

如果你需要在组件树中的多个组件中获取相同的数据,你不必全局获取数据并通过 props 传递下去。相反,你可以在需要数据的组件中获取数据,而不必担心多次请求相同数据的性能影响。

这是可能的,因为在 React 渲染过程中,具有相同 URL 和选项的 fetch 请求会自动被记忆。

了解更多关于 请求记忆 的信息。

ORM 和数据库客户端

你可以在 服务器组件路由处理程序服务器操作 中调用你的 ORM 或数据库客户端。

你可以使用 React cache 来在 React 渲染过程中记忆数据请求。例如,虽然 getItem 函数在布局和页面中都被调用,但只会对数据库进行一次查询:

app/utils.ts
import { cache } from "react";
 
export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id });
  return item;
});
app/utils.js
import { cache } from "react";
 
export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ id });
  return item;
});
app/item/[id]/layout.tsx
import { getItem } from "@/utils/get-item";
 
export default async function Layout({
  params: { id },
}: {
  params: { id: string };
}) {
  const item = await getItem(id);
  // ...
}
app/item/[id]/layout.js
import { getItem } from "@/utils/get-item";
 
export default async function Layout({ params: { id } }) {
  const item = await getItem(id);
  // ...
}
app/item/[id]/page.tsx
import { getItem } from "@/utils/get-item";
 
export default async function Page({
  params: { id },
}: {
  params: { id: string };
}) {
  const item = await getItem(id);
  // ...
}
app/item/[id]/page.js
import { getItem } from "@/utils/get-item";
 
export default async function Page({ params: { id } }) {
  const item = await getItem(id);
  // ...
}

你也可以使用实验性的 unstable_cacheunstable_noStore API 来配置这些请求的缓存和重新验证行为。

数据获取库

你可以在客户端组件中使用数据获取库,如 SWRReact Query 来获取数据。这些库提供了自己的 API 用于缓存、重新验证和变更数据。

例如,使用 SWR 在客户端定期获取数据:

app/page.tsx
"use client"
 
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
 
export default function PollingComponent {
  // 轮询间隔设置为 2000 毫秒
  const { data } = useSWR('/api/data', fetcher, { refreshInterval: 2000 });
 
  return '...'
}
app/page.tsx
"use client"
 
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
 
export default function PollingComponent {
  // 轮询间隔设置为 2000 毫秒
  const { data } = useSWR('/api/data', fetcher, { refreshInterval: 2000 });
 
  return '...'
}

路由处理程序

如果你需要创建 API 端点,Next.js 支持 路由处理程序。路由处理程序在服务器上执行,防止敏感信息 (例如 API 凭证) 暴露给客户端。

例如,使用 SWR 调用一个路由处理程序:

app/ui/message.tsx
"use client";
 
import useSWR from "swr";
import fetcher from "@/utils/fetcher";
 
export default function Message() {
  const { data } = useSWR("/api/messages", fetcher);
 
  return "...";
}
app/ui/message.js
"use client";
 
import useSWR from "swr";
import fetcher from "@/utils/fetcher";
 
export default function Message() {
  const { data } = useSWR("/api/messages", fetcher);
 
  return "...";
}
app/api/messages/route.ts
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 });
}
app/api/messages/route.js
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 });
}

查看 路由处理程序 文档以获取更多示例。

值得注意的是:由于服务器组件在服务器上渲染,你不需要从服务器组件调用路由处理程序。你可以直接访问你的数据。

模式

并行和顺序数据获取

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

顺序和并行数据获取
  • 顺序:组件树中的请求相互依赖。这可能导致较长的加载时间。
  • 并行:路由中的请求被急切地发起,并将同时加载数据。这减少了加载数据所需的总时间。

顺序数据获取

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

可能有一些情况下你想要这种模式,因为一个获取依赖于另一个的结果。例如,Playlists 组件只有在 Artist 组件完成数据获取后才会开始获取数据,因为 Playlists 依赖于 artistID 属性:

app/artist/[username]/page.tsx
export default async function Page({
  params: { username },
}: {
  params: { username: string };
}) {
  // 获取艺术家信息
  const artist = await getArtist(username);
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* 在 Playlists 组件加载时显示回退 UI */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* 将艺术家 ID 传递给 Playlists 组件 */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  );
}
 
async function Playlists({ artistID }: { artistID: string }) {
  // 使用艺术家 ID 获取播放列表
  const playlists = await getArtistPlaylists(artistID);
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  );
}
app/artist/[username]/page.js
export default async function Page({ params: { username } }) {
  // 获取艺术家信息const artist = await getArtist(username);
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* 在 Playlists 组件加载时显示回退 UI */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* 将艺术家 ID 传递给 Playlists 组件 */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  );
}
 
async function Playlists({ artistID }) {
  // 使用艺术家 ID 获取播放列表
  const playlists = await getArtistPlaylists(artistID);
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  );
}

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

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

并行数据获取

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

然而,由于 async/await 的性质,同一段或组件内的等待请求将阻塞其下方的任何请求。

要并行获取数据,你可以通过在使用数据的组件外部定义请求来急切地发起它们。这通过并行发起两个请求来节省时间,但是用户在两个 promise 都解析之前不会看到渲染结果。

在下面的例子中,getArtistgetAlbums 函数在 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: { username },
}: {
  params: { username: string };
}) {
  const artistData = getArtist(username);
  const albumsData = getAlbums(username);
 
  // 并行发起两个请求
  const [artist, albums] = await Promise.all([artistData, albumsData]);
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  );
}
app/artist/[username]/page.js
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);
 
  // 并行发起两个请求
  const [artist, albums] = await Promise.all([artistData, albumsData]);
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  );
}

此外,你可以添加一个 Suspense 边界 来分解渲染工作,并尽快显示部分结果。

预加载数据

防止瀑布的另一种方法是使用 预加载 模式,通过创建一个实用函数,你可以在阻塞请求之上急切地调用它。例如,checkIsAvailable() 阻塞 <Item/> 渲染,所以你可以在它之前调用 preload() 来急切地发起 <Item/> 的数据依赖。当 <Item/> 被渲染时,它的数据已经被获取了。

注意,preload 函数不会阻塞 checkIsAvailable() 的运行。

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);
  // ...
}
components/Item.js
import { getItem } from "@/utils/get-item";
 
export const preload = (id) => {
  // void 计算给定表达式并返回 undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id);
};
export default async function Item({ id }) {
  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;
}
app/item/[id]/page.js
import Item, { preload, checkIsAvailable } from "@/components/Item";
 
export default async function Page({ params: { id } }) {
  // 开始加载项目数据
  preload(id);
  // 执行另一个异步任务
  const isAvailable = await checkIsAvailable();
 
  return isAvailable ? <Item id={id} /> : null;
}

值得注意的是:"preload" 函数可以有任何名称,因为它是一种模式,而不是 API。

使用 React cacheserver-only 与预加载模式

你可以结合 cache 函数、预加载模式和 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.js
import { cache } from "react";
import "server-only";
 
export const preload = (id) => {
  void getItem(id);
};
 
export const getItem = cache(async (id) => {
  // ...
});

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

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

值得注意的是

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

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

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

要在你的应用中启用污点,将 Next.js 配置 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(
    "不要将整个用户对象传递给客户端",
    data
  );
  experimental_taintUniqueValue(
    "不要将用户的地址传递给客户端",
    data,
    data.address
  );
  return data;
}
app/utils.js
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;
}
app/page.tsx
import { getUserData } from "./data";
 
export async function Page() {
  const userData = getUserData();
  return (
    <ClientComponent
      user={userData} // 这将因 taintObjectReference 而导致错误
      address={userData.address} // 这将因 taintUniqueValue 而导致错误
    />
  );
}
app/page.js
import { getUserData } from "./data";
 
export async function Page() {
  const userData = await getUserData();
  return (
    <ClientComponent
      user={userData} // 这将因 taintObjectReference 而导致错误
      address={userData.address} // 这将因 taintUniqueValue 而导致错误
    />
  );
}