本页将引导您了解如何在服务端和客户端组件中获取数据,以及如何流式传输依赖于数据的组件
获取数据
服务端组件
您可以使用以下方式获取服务器组件中的数据:
fetch
API- ORM 或数据库
使用 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>
)
}
温馨提示:
- 默认情况下,
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>
)
}
客户端组件
有两种方法可以在客户端组件中获取数据:
- React 的 [[003.APIs#use实验特性|
use
hook]] - 使用像 SWR、React Query 这样的社区库
使用 use
hook 进行流式传输数据
你可以使用 React 的 use
hook 将数据从服务器流式传输到客户端。首先在服务端组件中获取数据,然后将 Promise 作为 prop 传递给客户端组件:
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// 不要 await,将 Promise 传递
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>
)
}
在上面的例子中, <Posts>
组件被 <Suspense>
边界包裹。这意味着在 Promise 被解析时将显示回退。了解更多关于流式传输的信息
社区库
您可以使用像 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>
)
}
删除重复请求并缓存数据
删除重复 fetch
请求的一种方法是使用请求记忆化 (request memoization)。使用此机制,在单个渲染过程中,使用相同 URL 和参数的 GET
或 HEAD
请求,fetch
调用会被合并为一个请求。此操作会自动执行,您也可以通过向 fetch
传递 Abort 信号来选择退出
请求记忆的范围限定在请求的生命周期内
您还可以使用 Next.js 的数据缓存来删除重复的 fetch
请求,例如在 fetch
选项中设置 cache: 'force-cache'
数据缓存允许在当前渲染过程和传入请求之间共享数据。
如果你不使用 fetch
,而是直接使用 ORM 或数据库,你可以使用 React cache
功能来包装数据访问
// app/lib/data.ts
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)),
})
})
流式传输
警告
以下内容假定您的应用程序中已启用
cacheComponents
配置选项。该标志是在 Next.js 15 canary 版本中引入的
在服务器组件中使用 async/await
时,Next.js 将启用动态渲染。这意味着每次用户请求时,都会在服务器上获取并渲染数据。如果有任何缓慢的数据请求,整个路由将被阻止渲染
为了改善初始加载时间和用户体验,您可以使用流式传输将页面的 HTML 分解为更小的块,并逐步将这些块从服务器发送到客户端
您可以通过两种方式在应用程序中实现流式传输:
- 使用
loading.js
文件包装页面 - 使用 [[002.前端/008.React/001.官方文档/001.React/002.Components#suspense|
<Suspense>
]] 包装组件
使用 loading.js
您可以在页面所在的文件夹中创建一个 loading.js
文件,以便在数据获取过程中流式传输整个页面
在导航时,用户将立即看到页面渲染时的布局和加载状态。渲染完成后,新内容将自动替换
在后台,loading.js
将嵌套在 layout.js
中,并将自动将 page.js
文件及其下面的任何子文件包裹在 <Suspense>
边界中
这种方法适用于路由段(Layouts and Pages),但对于更精细的流式传输,您可以使用 <Suspense>
使用 <Suspense>
<Suspense>
让您可以更精细地选择要流式传输的页面部分。例如,您可以立即显示 <Suspense>
边界之外的任何页面内容,并流式传输边界内的博客文章列表
// app/blog/page.tsx
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* 此内容将立即发送给客户端 */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* 任何被 <Suspense> 边界包裹的内容都将被流式传输 */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
创建有意义的加载状态
即时加载状态是指导航结束后立即向用户显示的后备界面。为了获得最佳用户体验,我们建议设计有意义的加载状态,帮助用户了解应用正在响应
例如,您可以使用骨架和旋转器,或者未来屏幕中虽小但有意义的部分(如封面照片、标题等)
在开发过程中,你可以使用 React Devtools 预览和检查组件的加载状态
示例
顺序 fetch 数据
当树中的嵌套组件各自 fetch 自己的数据并且请求未进行重复数据删除时,就会发生顺序数据获取,从而导致更长的响应时间
在某些情况下,您可能需要这种模式,因为一次 fetch 取决于另一次 fetch 的结果
为了提升用户体验,你应该使用 <Suspense>
在数据获取过程中显示 fallback
。这将启用流式传输,并防止整个路由被连续的数据请求阻塞
并行 fetch 数据
当路由中的数据请求被急切地发起并同时开始时,就会发生并行数据获取
默认情况下, 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
预加载数据
您可以通过创建一个函数来预加载数据,并在阻止请求上方急切调用该函数。 <Item>
根据 checkIsAvailable()
函数有条件地进行渲染
您可以在 checkIsAvailable()
之前调用 preload()
来预先初始化 <Item/>
数据依赖项。当 <Item/>
被渲染时,其数据已经获取完毕
// app/item/[id]/page.tsx
import { getItem, checkIsAvailable } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// 开始加载 item 数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id: string) => {
void getItem(id)
}
export async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
此外,你可以使用 React 的 [[003.APIs#cache实验特性|cache
]] 以及 server-only
包创建可重用的函数。这种方法允许您缓存数据获取函数,并确保它仅在服务器上执行
import 'server-only'
import { cache } from 'react'
import { getItem } from '@/lib/data'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})