外观
数据获取和缓存
数据获取和缓存
示例
本指南将带您了解 Next.js 中数据获取和缓存的基础知识,提供实用示例和最佳实践。
以下是 Next.js 中数据获取的最小示例:
app/page.tsx
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
app/page.js
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
这个示例展示了在异步 React 服务器组件中使用 fetch
API 进行基本的服务器端数据获取。
参考
fetch
- React
cache
- Next.js
unstable_cache
示例
使用 fetch
API 在服务器上获取数据
这个组件将获取并显示博客文章列表。来自 fetch
的响应将自动缓存。
app/page.tsx
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
app/page.js
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
如果您在应用程序的其他地方没有使用任何动态函数,这个页面将在 next build
期间被预渲染为静态页面。然后可以使用增量静态再生更新数据。
如果您不想缓存 fetch
的响应,可以这样做:
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
使用 ORM 或数据库在服务器上获取数据
这个组件将始终获取并显示动态的、最新的博客文章列表。
app/page.tsx
import { db, posts } from '@/lib/db'
export default async function Page() {
let allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
app/page.js
import { db, posts } from '@/lib/db'
export default async function Page() {
let allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
数据库调用不会被缓存。这个示例会将您的 Next.js 应用程序选择为服务器端渲染。如果您想缓存响应并允许页面被预渲染,请参阅此示例。
在客户端获取数据
我们建议首先尝试在服务器端获取数据。
然而,仍然有一些情况下客户端数据获取是有意义的。在这些场景中,您可以在 useEffect
中手动调用 fetch
(不推荐),或依赖社区中流行的 React 库(如 SWR 或 React Query)进行客户端获取。
app/page.tsx
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
let res = await fetch('https://api.vercel.app/blog')
let data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
app/page.js
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
let res = await fetch('https://api.vercel.app/blog')
let data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
使用 ORM 或数据库缓存数据
您可以使用 unstable_cache
API 来缓存响应,允许在运行 next build
时预渲染页面。
app/page.tsx
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
export default async function Page() {
const allPosts = await getPosts()
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
app/page.js
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
export default async function Page() {
const allPosts = await getPosts()
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
这个示例将数据库查询的结果缓存 1 小时(3600 秒)。它还添加了缓存标签 posts
,然后可以使用增量静态再生进行失效。
跨多个函数重用数据
Next.js 使用了像 generateMetadata
和 generateStaticParams
这样的 API,您需要在其中使用与 page
中相同的获取数据。
如果您使用 fetch
,请求会自动记忆化。这意味着您可以安全地使用相同的 URL 和选项多次调用,只会发出一个请求。
app/page.tsx
import { notFound } from 'next/navigation'
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
let res = await fetch(`https://api.example.com/posts/${id}`)
let post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
let posts = await fetch('https://api.example.com/posts').then((res) =>
res.json()
)
return posts.map((post: Post) => ({
id: post.id,
}))
}
export async function generateMetadata({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return {
title: post.title,
}
}
export default async function Page({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
app/page.js
import { notFound } from 'next/navigation'
async function getPost(id) {
let res = await fetch(`https://api.example.com/posts/${id}`)
let post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
let posts = await fetch('https://api.example.com/posts').then((res) =>
res.json()
)
return posts.map((post) => ({
id: post.id,
}))
}
export async function generateMetadata({ params }) {
let post = await getPost(params.id)
return {
title: post.title,
}
}
export default async function Page({ params }) {
let post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
如果您不使用 fetch
,而是直接使用 ORM 或数据库,您可以使用 React cache
函数包装您的数据获取。这将去重并只进行一次查询。
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db' // Example with Drizzle ORM
import { notFound } from 'next/navigation'
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
if (!post) notFound()
return post
})
重新验证缓存的数据
了解更多关于使用增量静态再生重新验证缓存数据的信息。
模式
并行和顺序数据获取
在组件内获取数据时,您需要注意两种数据获取模式:并行和顺序。
- 顺序:组件树中的请求相互依赖。这可能导致加载时间较长。
- 并行:路由中的请求被急切地发起,并将同时加载数据。这减少了加载数据所需的总时间。
顺序数据获取
如果您有嵌套组件,并且每个组件都获取自己的数据,那么如果这些数据请求没有被记忆化,数据获取将按顺序进行。
在某些情况下,您可能希望使用这种模式,因为一个获取依赖于另一个的结果。例如,Playlists
组件只有在 Artist
组件完成数据获取后才会开始获取数据,因为 Playlists
依赖于 artistID
属性:
app/artist/[username]/page.tsx
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>
)
}
app/artist/[username]/page.js
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>
(用于嵌套组件)来显示即时加载状态,同时 React 流式传输结果。
这将防止整个路由被数据请求阻塞,用户将能够与页面准备好的部分进行交互。
并行数据获取
默认情况下,布局和页面段并行渲染。这意味着请求将并行发起。
然而,由于 async
/await
的性质,同一段或组件内的等待请求将阻塞其下的任何请求。
要并行获取数据,您可以通过在使用数据的组件外部定义它们来急切地发起请求。这通过并行发起两个请求来节省时间,但用户在两个 promise 都解析之前不会看到渲染结果。
在下面的示例中,getArtist
和 getAlbums
函数在 Page
组件外部定义,并使用 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: { 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} />
</>
)
}
app/artist/[username]/page.js
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()
运行。
components/Item.tsx
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)
// ...
}
components/Item.js
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)
// ...
}
app/item/[id]/page.tsx
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
}
app/item/[id]/page.js
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。
使用 React cache
和 server-only
与预加载模式
您可以结合使用 cache
函数、preload
模式和 server-only
包,创建一个可以在整个应用程序中使用的数据获取实用程序。
utils/get-item.ts
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
utils/get-item.js
import { cache } from 'react'
import 'server-only'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})
使用这种方法,您可以急切地获取数据、缓存响应,并确保这种数据获取仅在服务器上发生。
utils/get-item
导出可以被布局、页面或其他组件使用,以便给它们控制何时获取 item 的数据。
提示
我们建议使用 server-only
包 确保服务器数据获取函数永远不会在客户端使用。
防止敏感数据泄露给客户端
我们建议使用 React 的污染 API,taintObjectReference
和 taintUniqueValue
,以防止整个对象实例或敏感值传递给客户端。
要在应用程序中启用污染,请将 Next.js 配置 experimental.taint
选项设置为 true
:
next.config.js
module.exports = {
experimental: {
taint: true,
},
}
然后将您想要污染的对象或值传递给 experimental_taintObjectReference
或 experimental_taintUniqueValue
函数:
app/utils.ts
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
}
app/utils.js
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
}
app/page.tsx
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
/>
)
}
app/page.js
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
/>
)
}