数据获取
数据获取是一切应用程序的核心部分。本页详细演示了使用您喜欢的方法的数据演示的最佳实践。
我应该在服务器还是在客户端获取数据?
Deciding whether to fetch data on the server or the client depends on the type of UI you're building. 您正在构建的 UI 类型决定了在服务器还是在客户端获取数据。
大多数情况下, 当您不需要实时数据(例如 polling)时, 您可以使用 服务器组件在服务器上获取数据。这样做有几个好处:
- 您可以在单次服务器往返中获取数据,从而减少了网络请求和客户端-服务器瀑布的数量。
- 防止敏感信息(例如 access tokens 和 API keys)暴露给客户端(这需要一个中间的 API 路由)。
- 通过靠近源获取数据减少延迟 (如果您的应用程序代码和数据库在同一个区域)。
- 数据请求可以被缓存 和 重新验证。
然而, 服务器端数据获取会导致整个页面在服务器上被重新渲染。 当您需要改变/重新验证较小的 UI 部分或持续获取实时数据时(例如实时视图), 客户端数据获取可能会更适合,因为它会让您在客户端重新渲染具体的 UI 部分。
Next.js 中数据获取方式有四种:
fetchAPI on the server。- 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 Web API, fetch 允许您为服务器上的每个 fetch 请求配置缓存 和 重新验证行为。您可以在服务器组件, 路由处理程序, 和 服务器操作中使用fetch。例如:
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请求检索最新数据。使用它会导致整个路由动态渲染并且不会缓存数据。
您可以通过对force-cache设置cache来缓存fetch请求。这意味着数据会被缓存、组件会被静态渲染:
fetch("https://...", { cache: "force-cache" });
或者, 如果使用PPR, 我们推荐将使用fetch请求的组件包装在 Suspense 边界内。这会确保只有使用了fetch的组件会被动态渲染和流式传输, 而不是整个页面:
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会被缓存。参阅升级指导获取更多信息。
Request Memoization
如果您需要在树中的多个组件内获取相同的数据,您不需要全局获取数据并向下传递 props。相反,您可以在需要数据的组件内获取数据,无需担心为获取同样的数据而发送多个请求带来的性能影响。
这是可行的,因为带有相同 URL 和选项的fetch请求在 React 渲染过程中被自动记住。
了解更多关于request memoization.
ORMs 和数据库客户端
您可以在服务器组件, 路由处理程序, 和 服务器操作中调用 ORM 或数据库客户端。
您可以使用React cache在 React 渲染过程中记忆数据请求。例如, 尽管在 layout 和 page 中都调用了getItem function, 但是只会对数据库进行一次查询:
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 APIs 来配置这些请求的缓存和重新验证行为。
Data Fetching Libraries
您可以在客户端组件中使用数据获取库(例如:SWR 或 React Query)来获取数据。这些库为缓存、重新验证和改变数据提供了它们自己的 API。
例如, 使用 SWR 在客户端定期获取数据:
"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 credentials).
例如, 使用 SWR 调用路由处理程序:
"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 });
}
参阅路由处理程序文档获取更多信息。
您需要知道: 因为服务器组件在服务器上运行, 所以您不需要从服务器组件调用路由处理程序。您可以直接访问您的数据。
模式
平行和顺序获取数据
在组件内获取数据时, 您需要知道的两种数据获取模式:平行和顺序。
- 顺序: 组件树内的请求是相互依赖的。这可能会导致较长的加载时间。
- 平行: 路由内的请求会立即发起,与此同时将会加载数据。这减少了数据加载所需要的总时间。
Sequential data fetching
如果您使用嵌套组件, 并且每个组件都会获取自身数据, 那么如果这些数据请求没有被记忆,数据获取就会按顺序发生。
在某些情况下,您可能需要这种模式,因为一个 fetch 依赖于另一个 fetch 的请求结果。例如,Playlists组件只会在Artist组件已经完成数据获取之后才开始获取数据,因为Playlists需要artistID属性:
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> (用于嵌套路由)来展示即时加载状态, while React streams in the result.
这可以避免整个路由被数据请求阻塞,并且用户能够与页面中加载好的部分进行交互。
Parallel Data Fetching
默认情况下, layout 和 page 段平行渲染。这意味着请求将被并行发起。
然而, 由于async/await的特点, 同一个段或组件内等待的请求将会阻塞它后面的请求。
为了并 行获取数据, 您可以通过在使用这个数据的组件外定义它们来主动发起请求。这通过并行地启动两个请求来节省时间, 然而, 直到两个 promises 都解析完,用户才会看到渲染结果。
下列例子中, getArtist 和 getAlbums 函数在Page组件外被定义,并在组件内部使用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} />
</>
);
}
此外, 您可以添加Suspense 边界来分解渲染工作并快速显示部分结果。
预加载数据
防止瀑布流的另一种方法是通过创建一个在阻塞请求之前就立即调用的实用函数,来使用预加载方式。 例如, checkIsAvailable() 阻塞 <Item/> 渲染, 因此您可以在它之前调用preload()来立即发起<Item/> 数据依赖。 到渲染<Item/>时, 其数据已被获取。
注意preload 函数不会阻塞checkIsAvailable()运行。
import { getItem } from "@/utils/get-item";
export const preload = (id: string) => {
// void evaluates the given expression and returns 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);
// ...
}
import { getItem } from "@/utils/get-item";
export const preload = (id) => {
// void evaluates the given expression and returns 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);
// ...
}
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。
Reactcache、server-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) => {
// ...
});
import { cache } from "react";
import "server-only";
export const preload = (id) => {
void getItem(id);
};
export const getItem = cache(async (id) => {
// ...
});
通过这个方法, 您可以快速获取数据、缓存响应和保证数据获取只发生在服务器上。
Layouts、Pages 或其它组件能够使用导出的utils/get-item来获得何时一项数据被获取的控制。
您需要知道:
- 我们推荐使用
server-only包来确保服务器数据获取功能从不会在客户端使用。
防止敏感数据暴露给客户端
我们推荐使用 React's taint APIs, taintObjectReference 和 taintUniqueValue, 来防止整个对象实例或敏感值被传递给客户端。
为了在应用程序中启动 tainting, 将 Next.js Config experimental.taint选项设置为true:
module.exports = {
experimental: {
taint: true,
},
};
然后将你要暴露的对象或值传递给experimental_taintObjectReference 或 experimental_taintUniqueValue 函数:
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
/>
);
}