如何获取数据并实现流式传输
本页将指导您如何在 服务端与客户端组件 中获取数据,以及如何流式传输依赖数据的组件。
数据获取
服务端组件
您可以在服务端组件中使用以下方式获取数据:
使用 fetch
API
要通过 fetch
API 获取数据,将组件转为异步函数并等待 fetch
调用。例如:
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
须知:
fetch
响应默认不缓存。但 Next.js 会 预渲染 路由并将输出缓存以提升性能。如需启用 动态渲染,请使用{ cache: 'no-store' }
选项。参阅fetch
API 参考。- 开发阶段可记录
fetch
调用以便调试,参阅logging
API 参考。
使用 ORM 或数据库
由于服务端组件在服务器渲染,可安全使用 ORM 或数据库客户端查询。将组件转为异步函数并等待调用:
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
客户端组件
在客户端组件获取数据有两种方式:
- 使用 React 的
use
钩子 - 使用社区库如 SWR 或 React Query
使用 use
钩子流式传输数据
可通过 React 的 use
钩子 从服务端 流式传输 数据到客户端。先在服务端组件获取数据,将 promise 作为 prop 传递给客户端组件:
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// 不要等待数据获取函数
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// 不要等待数据获取函数
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
然后在客户端组件使用 use
钩子读取 promise:
'use client'
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import { use } from 'react'
export default function Posts({ posts }) {
const posts = use(posts)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
上例中 <Posts>
组件被包裹在 <Suspense>
边界 内,promise 解析期间会显示 fallback。了解更多关于 流式传输。
社区库
可使用社区库如 SWR 或 React Query 在客户端组件获取数据。这些库有各自的缓存、流式传输等特性。例如使用 SWR:
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
使用 React.cache
去重请求
去重是在渲染过程中 防止对同一资源发起重复请求 的过程。它允许在不同组件中获取相同数据,同时防止向数据源发起多次网络请求。
如果使用 fetch
,可通过添加 cache: 'force-cache'
去重请求。这意味着可以安全地使用相同 URL 和选项调用,仅会发起一次请求。
如果 不使用 fetch
,而是直接使用 ORM 或数据库,可以用 React cache
函数包裹数据获取。
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
export const getPost = cache(async (id: string) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
流式传输
警告: 以下内容假设您的应用已启用
dynamicIO
配置选项。该标志在 Next.js 15 canary 中引入。
在服务端组件使用 async/await
时,Next.js 会启用 动态渲染。这意味着数据会在服务器端为每个用户请求获取和渲染。如果有慢速数据请求,整个路由的渲染会被阻塞。
为提升初始加载时间和用户体验,可使用流式传输将页面 HTML 拆分为小块,逐步从服务器发送到客户端。

有两种方式在应用中实现流式传输:
- 用
loading.js
文件 包裹页面 - 用
<Suspense>
包裹组件
使用 loading.js
可在页面同级目录创建 loading.js
文件,在数据获取期间流式传输 整个页面。例如要流式传输 app/blog/page.js
,在 app/blog
文件夹添加该文件。

export default function Loading() {
// 在此定义加载 UI
return <div>Loading...</div>
}
export default function Loading() {
// 在此定义加载 UI
return <div>Loading...</div>
}
导航时用户会立即看到布局和 加载状态,页面渲染完成后新内容会自动替换。

在底层,loading.js
会嵌套在 layout.js
内,并自动将 page.js
文件及其子内容包裹在 <Suspense>
边界中。

此方法适用于路由段(布局和页面),如需更细粒度流式传输,可使用 <Suspense>
。
使用 <Suspense>
<Suspense>
允许更精细控制页面哪些部分需要流式传输。例如可立即显示 <Suspense>
边界外的页面内容,边界内的博客列表则流式传输。
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* 这部分内容会立即发送到客户端 */}
<header>
<h1>欢迎来到博客</h1>
<p>阅读最新文章如下。</p>
</header>
<main>
{/* 任何包裹在 <Suspense> 边界的内容将流式传输 */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* 这部分内容会立即发送到客户端 */}
<header>
<h1>欢迎来到博客</h1>
<p>阅读最新文章如下。</p>
</header>
<main>
{/* 任何包裹在 <Suspense> 边界的内容将流式传输 */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
创建有意义的加载状态
即时加载状态是导航后立即向用户显示的 fallback UI。为最佳用户体验,建议设计能帮助用户理解应用正在响应的有意义加载状态。例如使用骨架屏、旋转器,或未来屏幕的小但有意义部分如封面图、标题等。
开发阶段可使用 React Devtools 预览和检查组件的加载状态。
示例
顺序数据获取
当树中的嵌套组件各自获取数据且请求未 去重 时,会发生顺序数据获取,导致响应时间延长。

某些情况下可能需要此模式,因为一次获取依赖另一次的结果。
例如 <Playlists>
组件仅在 <Artist>
组件完成数据获取后开始,因为 <Playlists>
依赖 artistID
prop:
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// 获取艺人信息
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* 在 Playlists 组件加载时显示 fallback 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>
)
}
export default async function Page({ params }) {
const { username } = await params
// 获取艺人信息
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* 在 Playlists 组件加载时显示 fallback 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>
)
}
为提升用户体验,应使用 React <Suspense>
在数据获取时显示 fallback
。这将启用 流式传输 并防止顺序数据请求阻塞整个路由。
并行数据获取
当路由中的数据请求被主动发起并同时开始时,就会发生并行数据获取。
默认情况下,布局和页面 (layouts and pages) 是并行渲染的。因此每个路由段会尽可能早地开始获取数据。
然而,在_任何_组件中,如果多个 async
/await
请求被顺序放置,它们仍会按顺序执行。例如,getAlbums
会一直阻塞直到 getArtist
完成:
import { getArtist, getAlbums } from '@/app/lib/data'
export default async function Page({ params }) {
// 这些请求会顺序执行
const { username } = await params
const artist = await getArtist(username)
const albums = await getAlbums(username)
return <div>{artist.name}</div>
}
你可以通过在数据使用组件外部定义请求,并使用 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,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// 并行发起两个请求
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 }) {
const { username } = await params
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// 并行发起两个请求
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
须知: 当使用
Promise.all
时,如果其中一个请求失败,整个操作都会失败。要处理这种情况,可以使用Promise.allSettled
方法替代。
数据预加载
你可以创建一个工具函数来预加载数据,并在阻塞请求之前主动调用它。<Item>
组件会根据 checkIsAvailable()
函数的返回值条件渲染。
你可以在 checkIsAvailable()
之前调用 preload()
来主动加载 <Item/>
的数据依赖。当 <Item/>
渲染时,其数据已经获取完成。
import { getItem } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// 开始加载项目数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id: string) => {
// void 会执行给定的表达式并返回 undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
import { getItem } from '@/lib/data'
export default async function Page({ params }) {
const { id } = await params
// 开始加载项目数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id) => {
// void 会执行给定的表达式并返回 undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }) {
const result = await getItem(id)
// ...
此外,你还可以使用 React 的 cache
函数 和 server-only
包 来创建一个可复用的工具函数。这种方法可以缓存数据获取函数并确保它只在服务器端执行。
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})