获取数据
原文地址:https://nextjs.org/docs/app/getting-started/fetching-data
本页将引导您了解如何在服务器和客户端组件中获取数据,以及如何流式传输依赖于数据的组件。
1. 获取数据
1.1 服务端组件
您可以使用任何异步 I/O 在服务器组件中获取数据,例如:
fetch API 接口;
- ORM 或数据库
- 使用 Node.js API(例如
fs 从文件系统读取数据)
1.1.1 使用 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}>nnnnhhhyjjkkfgvrx{post.title}</li>
))}
</ul>
)
}
|
备注
默认情况下不缓存 fetch 响应。但是,Next.js 将预渲染路由,并且输出将被缓存以提高性能。如果你想选择动态渲染,请使用 { cache: 'no-store' } 选项。请参考 fetch API。
在开发过程中,您可以记录 fetch 调用以获得更好的可见性和调试。请参阅 logging API 参考。
1.1.2 使用 ORM 或数据库
由于服务端组件是在服务器上呈现的,因此我们可以使用 ORM 或数据库客户端安全地进行数据库查询。将服务端组件转换为异步函数,并等待调用:
| app/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.2 客户端组件
有两种方法可以在客户端组件中获取数据,使用:
- React 的
use 钩子
- 像 SWR 或 React Query 这样的社区库
1.2.1 使用 use 钩子流式传输数据
我们可以使用 React 的 use 钩子将数据从服务端流式传输到客户端。首先在服务端组件中获取数据,并将 Promise 作为 prop 传递给客户端组件:
| app/blog/page.tsx |
|---|
| import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
export default function Page() {
// Don't await the data fetching function
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>
)
}
|
1.2.2 使用社区库
您可以使用 SWR 或 React 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>
)
}
|
2. 重复请求和缓存数据
消除重复 fetch 请求的一种方法是使用请求记忆,这种机制使得具有相同 URL 的 GET 或 HEAD 的 fetch 调用以及单个渲染过程中的选项将合并到一个请求中。这是自动发生的,我们可以通过向 fetch 传递 Abort 信号来选择退出。
请求记忆的范围是请求的生命周期。
您还可以使用 Next.js 的数据缓存来删除重复的 fetch 请求,例如通过在 fetch 选项中设置 cache: 'force-cache'。
数据缓存允许在当前渲染通道和传入请求之间共享数据。
如果您不使用 fetch,而是直接使用 ORM 或数据库,则可以使用 React cache 功能包装数据访问。
| app/lib/db.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)),
})
})
|
3. 流式
警告
以下内容假设您的应用程序中启用了 cacheComponents 配置选项。该标志是在 Next.js 15 canary 中引入的。
当您在服务器组件中获取数据时,系统会针对每个请求在服务器上获取并呈现数据。如果您有任何缓慢的数据请求,则整个路由将被阻止渲染,直到获取所有数据为止。
为了改善初始加载时间和用户体验,您可以使用流式传输将页面的 HTML 分解为更小的块,并逐步将这些块从服务器发送到客户端。
您可以通过两种方式在应用程序中利用流式传输:
- 用
loading.js 文件包装页面
- 用
<Suspense> 包装组件
3.1 使用 loading.js
您可以在页面所在的同一文件夹中创建 loading.js 文件,以便在获取数据时流式传输整个页面。例如,要流式传输 app/blog/page.js ,请将文件添加到 app/blog 文件夹中。
| app/blog/loading.tsx |
|---|
| export default function Loading() {
// Define the Loading UI here
return <div>Loading...</div>
}
|
在导航时,用户将在页面呈现时立即看到布局和加载状态。渲染完成后,新内容将自动交换。
在幕后, loading.js 将嵌套在 layout.js 中,并自动将 page.js 文件和下面的所有子文件包装在 <Suspense> 边界中。
此方法适用于路由段(布局和页面),但对于更细粒度的流,您可以使用 <Suspense> 。
3.2 使用 <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>
{/* This content will be sent to the client immediately */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* If there's any dynamic content inside this boundary, it will be streamed in */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
|
3.3 创建有意义的加载状态
即时加载状态是后备 UI,在导航后立即向用户显示。为了获得最佳的用户体验,我们建议设计有意义的加载状态并帮助用户了解应用程序正在响应。例如,您可以使用骨架和旋转器,或者未来屏幕的一小部分但有意义的部分,例如封面照片、标题等。
在开发过程中,您可以使用 React Devtools 预览和检查组件的加载状态。
4. 示例
4.1 顺序数据获取
当一个请求依赖于另一请求的数据时,就会发生顺序数据获取。
例如, <Playlists> 只能在 <Artist> 完成后获取数据,因为它需要 artistID:
| app/artist/[username]/page.tsx |
|---|
| export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// 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>
)
}
|
在此示例中, <Suspense> 允许播放列表在艺术家数据加载后流入。但是,页面在显示任何内容之前仍会等待 artist 数据。为了防止这种情况,您可以将整个页面组件包装在 <Suspense> 边界中(例如,使用 loading.js 文件)以立即显示加载状态。
确保您的数据源可以快速解决第一个请求,因为它会阻止其他所有请求。如果无法进一步优化请求,并且数据不经常更改,请考虑缓存结果。
4.2 并行数据获取
当路由中的数据请求被急切地发起并同时开始时,就会发生并行数据获取。
默认情况下,布局和页面是并行呈现的。所以每个段都会尽快开始获取数据。
但是,在任何组件内,多个 async / await 请求如果放置在另一个请求之后,仍然可以是连续的。例如, getAlbums将被阻塞,直到 getArtist 被解析:
| app/artist/[username]/page.tsx |
|---|
| import { getArtist, getAlbums } from '@/app/lib/data'
export default async function Page({ params }) {
// These requests will be sequential
const { username } = await params
const artist = await getArtist(username)
const albums = await getAlbums(username)
return <div>{artist.name}</div>
}
|
通过调用 fetch 启动多个请求,然后使用 Promise.all 等待它们。一旦调用 fetch,请求就会开始。
| 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
// Initiate requests
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 方法。
4.3 预加载数据
您可以通过创建一个在阻塞请求之上急切调用的实用函数来预加载数据。 <Item> 根据 checkIsAvailable() 函数有条件地呈现。
您可以在 checkIsAvailable() 之前调用 preload() 来立即启动 <Item/> 数据依赖关系。当 <Item/> 被渲染时,它的数据已经被获取。
| app/page.tsx |
|---|
| import { getItem, checkIsAvailable } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
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 async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
|
此外,您可以使用 React 的 cache 功能和 server-only 包来创建可重用的实用程序功能。这种方法允许您缓存数据获取函数并确保它仅在服务器上执行。
| app/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) => {
// ...
})
|