跳到主要内容

获取数据

本页将指导你如何在服务端和客户端组件中获取数据,以及如何流式传输依赖于数据的组件。

获取数据

服务端组件

你可以在服务端组件中使用以下方式获取数据:

  1. fetch API
  2. ORM 或数据库

使用 fetch API

要使用 fetch API 获取数据,将你的组件转换为异步函数,并等待 fetch 调用。例如:

app/blog/page.tsx
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 或数据库客户端进行数据库查询。将你的组件转换为异步函数,并等待调用:

app/blog/page.tsx
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>
)
}

客户端组件

在客户端组件中有两种获取数据的方式,使用:

  1. React 的 use 钩子
  2. 社区库如 SWRReact Query

使用 use 钩子流式传输数据

你可以使用 React 的 use 钩子从服务器向客户端流式传输数据。首先在服务端组件中获取数据,然后将 promise 作为 prop 传递给客户端组件:

app/blog/page.tsx
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:

app/ui/posts.tsx
'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 解析期间将显示备用内容。了解更多关于流式传输

社区库

你可以使用社区库如 SWRReact Query 在客户端组件中获取数据。这些库有自己的缓存、流式传输和其他功能的语义。例如,使用 SWR:

app/blog/page.tsx
'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 请求的一种方法是使用请求记忆化。通过这种机制,在单个渲染过程中使用相同 URL 和选项的 GETHEADfetch 调用会被合并为一个请求。这是自动发生的,你可以通过向 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)),
})
})

流式传输

警告: 下面的内容假设你的应用程序中启用了 dynamicIO 配置选项。该标志是在 Next.js 15 canary 版本中引入的。

在服务端组件中使用 async/await 时,Next.js 会选择动态渲染。这意味着数据将在服务器上为每个用户请求获取和渲染。如果有任何慢速数据请求,整个路由将被阻止渲染。

为了改善初始加载时间和用户体验,你可以使用流式传输将页面的 HTML 分解为更小的块,并逐步将这些块从服务器发送到客户端。

服务端渲染与流式传输的工作原理服务端渲染与流式传输的工作原理

在你的应用程序中实现流式传输有两种方式:

  1. loading.js 文件包装页面
  2. <Suspense> 包装组件

使用 loading.js

你可以在页面所在的同一文件夹中创建 loading.js 文件,在获取数据时流式传输整个页面。例如,要流式传输 app/blog/page.js,请在 app/blog 文件夹内添加文件。

带有 loading.js 文件的博客文件夹结构带有 loading.js 文件的博客文件夹结构
app/blog/loading.tsx
export default function Loading() {
// 在此定义加载界面
return <div>Loading...</div>
}

在导航时,用户将立即看到布局和加载状态,而页面正在渲染。一旦渲染完成,新内容将自动交换进来。

加载界面加载界面

在幕后,loading.js 将嵌套在 layout.js 内,并自动将 page.js 文件及其下面的任何子级包装在 <Suspense> 边界中。

loading.js 概述loading.js 概述

这种方法适用于路由段(布局和页面),但对于更细粒度的流式传输,你可以使用 <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>欢迎来到博客</h1>
<p>在下面阅读最新文章。</p>
</header>
<main>
{/* 包装在 <Suspense> 边界中的任何内容都将被流式传输 */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}

创建有意义的加载状态

即时加载状态是在导航后立即显示给用户的备用界面。为了获得最佳用户体验,我们建议设计有意义的加载状态,帮助用户理解应用程序正在响应。例如,你可以使用骨架屏和旋转器,或者未来屏幕的小但有意义的部分,如封面照片、标题等。

在开发中,你可以使用 React Devtools 预览和检查组件的加载状态。

示例

顺序数据获取

顺序数据获取发生在树中的嵌套组件各自获取自己的数据且请求没有被去重时,导致响应时间更长。

顺序和并行数据获取顺序和并行数据获取

在某些情况下,你可能需要这种模式,因为一个获取依赖于另一个的结果。

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

app/artist/[username]/page.tsx
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 组件加载时显示备用界面 */}
<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>
)
}

为了改善用户体验,你应该使用 React <Suspense> 在获取数据时显示 fallback。这将启用流式传输并防止整个路由被顺序数据请求阻塞。

并行数据获取

并行数据获取发生在路由中的数据请求被急切地启动并同时开始时。

默认情况下,布局和页面并行渲染。因此每个段都会尽快开始获取数据。

但是,在任何组件内,如果多个 async/await 请求放在另一个之后,它们仍然可能是顺序的。例如,getAlbums 将被阻塞,直到 getArtist 解析完成:

app/artist/[username]/page.tsx
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

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,
}: {
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} />
</>
)
}

需要了解: 当使用 Promise.all 时,如果一个请求失败,整个操作将失败。为了处理这种情况,你可以使用 Promise.allSettled 方法替代。

预加载数据

你可以通过创建一个工具函数来预加载数据,该函数在阻塞请求之前急切地调用。<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
// 开始加载项目数据
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)
// ...
}

此外,你可以使用 React 的 cache 函数server-only创建一个可重用的工具函数。这种方法允许你缓存数据获取函数并确保它只在服务器上执行。

utils/get-item.ts
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) => {
// ...
})