在 Next.js 中如何获取数据呢?Next.js 优先推荐使用原生的 fetch 方法,因为 Next.js 拓展了原生的 fetch 方法,为其添加了缓存和更新缓存(重新验证)的机制,这样做的好处在于可以自动复用请求数据,提高性能。坏处在于你得多学一点关于如何更新缓存……
那就让我们来看看具体如何使用吧。
Next.js 拓展了原生的 fetch Web API,增加了缓存(caching)和重新验证( revalidating)功能。你可以在:
中搭配 async
/await
使用 fetch。举个例子:
// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/...')
if (!res.ok) {
// 这会触发最近的 `error.js` 错误边界
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
默认情况下,Next.js 会自动缓存服务端 fetch
的返回值,也就是说,数据会在构建或者请求的时候被缓存,后续相同的请求会直接使用缓存中的数据,这有利于提高应用的性能。
// fetch 的 cache 选项用于控制该请求的缓存行为
// 默认就是 'force-cache', 平时写的时候可以省略
fetch('https://...', { cache: 'force-cache' })
注:不仅 GET
请求会被缓存,正常使用 POST
方法的 fetch
请求也会被自动缓存,但在路由处理程序中使用 POST
方法的 fetch
请求不会被缓存。
有的时候缓存数据会过期,那么该如何更新缓存呢?在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation)。
Next.js 提供了两种方式更新缓存:
一种是基于时间的重新验证(Time-based revalidation),即经过一定时间并有新请求产生后重新验证数据,适用于不经常更改且新鲜度不那么重要的数据。
一种是按需重新验证(On-demand revalidation),根据事件手动重新验证数据。按需重新验证又可以使用基于标签(tag-based)和基于路径(path-based)两种方法重新验证数据。适用于需要尽快展示最新数据的场景。
使用基于时间的重新验证,你需要在使用 fetch 的时候设置 next.revalidate
选项(以秒为单位):
fetch('https://...', { next: { revalidate: 3600 } })
或者通过路由段配置项进行配置,使用这种方法,它会重新验证该路由段所有的 fetch
请求。
// layout.js | page.js
export const revalidate = 3600
注:在一个静态渲染的路由中,如果你有多个请求,每个请求设置了不同的重新验证时间,将会使用最短的时间用于所有的请求。而对于动态渲染的路由,每一个 fetch
请求都将独立重新验证。
使用按需重新验证,在路由处理程序或者 Server Action 中通过路径( revalidatePath) 或缓存标签 revalidateTag 实现。
Next.js 有一个路由标签系统,可以跨路由实现多个 fetch 请求重新验证。我们来具体介绍下这个过程:
revalidateTag
方法重新验证该标签对应的所有的请求举个例子:
// app/page.js
export default async function Page() {
const res = await fetch('https://...', { next: { tags: ['collection'] } })
const data = await res.json()
// ...
}
在这个例子中,你为 fetch
请求添加了一个 collection
标签。现在,你可以在 Server Action 中调用 revalidateTag
,就可以让所有带 collection
标签的 fetch 请求重新验证。
// app/actions.js
'use server'
import { revalidateTag } from 'next/cache'
export default async function action() {
revalidateTag('collection')
}
如果在尝试重新验证的过程中出现错误,缓存会继续提供上一个重新生成的数据,而在下一个后续请求中,Next.js 会尝试再次重新验证数据。
当 fetch
请求满足这些条件时不会被缓存:
fetch
请求添加了 cache: 'no-store'
选项fetch
请求添加了 revalidate: 0
选项fetch
请求在路由处理程序中并使用了 POST
方法headers
或 cookies
的方法之后使用 fetch
请求const dynamic = 'force-dynamic'
fetchCache
,默认会跳过缓存fetch
请求使用了 Authorization
或者 Cookie
请求头,并且在组件树中其上方还有一个未缓存的请求在具体使用的时候,如果你想不缓存某个单独请求:
// layout.js | page.js
fetch('https://...', { cache: 'no-store' })
不缓存多个请求,可以借助路由段配置项:
// layout.js | page.js
export const dynamic = 'force-dynamic'
Next.js 推荐单独配置每个请求的缓存行为,这可以让你更精细化的控制缓存行为。
也不是所有时候都能使用 fetch 请求,如果你使用了不支持或者暴露 fetch 方法的三方库(如数据库、CMS 或 ORM 客户端),但又想实现数据缓存机制,那你可以使用 React 的 cache
函数和路由段配置项来实现请求的缓存和重新验证。
举个例子:
// app/utils.js
import { cache } from 'react'
export const getItem = cache(async (id) => {
const item = await db.item.findUnique({ id })
return item
})
现在我们调用两次 getItem
:
// app/item/[id]/layout.js
import { getItem } from '@/utils/get-item'
export const revalidate = 3600 // 最多每小时重新验证一次
export default async function Layout({ params: { id } }) {
const item = await getItem(id)
// ...
}
// app/item/[id]/page.js
import { getItem } from '@/utils/get-item'
export const revalidate = 3600 // revalidate the data at most every hour
export default async function Page({ params: { id } }) {
const item = await getItem(id)
// ...
}
在这个例子中,尽管 getItem
被调用两次,但只会产生一次数据库查询。
如果你需要在客户端组件中获取数据,可以在客户端调用路由处理程序。路由处理程序会在服务端被执行,然后将数据返回给客户端,适用于不想暴露敏感信息给客户端(比如 API tokens)的场景。
如果你使用的是服务端组件,无须借助路由处理程序,直接获取数据即可。
你也可以在客户端使用三方的库如 SWR 或 React Query 来获取数据。这些库都有提供自己的 API 实现记忆请求、缓存、重新验证和更改数据。
有一些在 React 和 Next.js 中获取数据的建议和最佳实践,本节来介绍一下:
尽可能在服务端获取数据,这样做有很多好处,比如:
如果组件树中的多个组件使用相同的数据,无须先全局获取,再通过 props 传递,你可以直接在需要的地方使用 fetch
或者 React cache
获取数据,不用担心多次请求造成的性能问题,因为 fetch
请求会自动被记忆化。这也同样适用于布局,毕竟本来父子布局之间也不能传递数据。
Streaming 和 Suspense
都是 React 的功能,允许你增量传输内容以及渐进式渲染 UI 单元。页面可以直接渲染部分内容,剩余获取数据的部分会展示加载态,这也意味着用户不需要等到页面完全加载完才能与其交互。
关于 Streaming,我们会在小册 《渲染篇 | Streaming 和 Edge Runtime》详细讲解。
在 React 组件内获取数据时,有两种数据获取模式,并行和串行。
所谓串行数据获取,数据请求相互依赖,形成瀑布结构,这种行为有的时候是必要的,但也会导致加载时间更长。
所谓并行数据获取,请求同时发生并加载数据,这会减少加载数据所需的总时间。
我们先说说串行数据获取,直接举个例子:
// app/artist/page.js
// ...
async function Playlists({ artistID }) {
// 等待 playlists 数据
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({ params: { username } }) {
// 等待 artist 数据
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
在这个例子中,Playlists
组件只有当 Artist
组件获得数据才会开始获取数据,因为 Playlists
组件依赖 artistId
这个 prop。这也很容易理解,毕竟只有先知道了是哪位艺术家,才能获取这位艺术家对应的曲目。
在这种情况下,你可以使用 loading.js
或者 React 的 <Suspense>
组件,展示一个即时加载状态,防止整个路由被数据请求阻塞,而且用户还可以与未被阻塞的部分进行交互。
关于阻塞数据请求:
await
的 fetch
请求都会阻塞渲染和下方所有组件的数据请求,除非它们使用了 <Suspense>
或者 loading.js
。另一种替代方式就是使用并行数据请求或者预加载模式。要实现并行请求数据,你可以在使用数据的组件外定义请求,然后在组价内部调用,举个例子:
import Albums from './albums'
// 组件外定义
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getArtistAlbums(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 = getArtistAlbums(username)
// 等待 promise resolve
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
在这个例子中,getArtist
和 getArtistAlbums
函数都是在 Page
组件外定义,然后在 Page
组件内部调用。用户需要等待两个 promise 都 resolve 后才能看到结果。
为了提升用户体验,可以使用 Suspense 组件来分解渲染工作,尽快展示出部分结果。
防止出现串行请求的另外一种方式是使用预加载。你可以创建一个 preload
函数进一步优化并行数据获取。使用这种方式,你不需要再使用 props 往下传递,举个例子:
// components/Item.js
import { getItem } from '@/utils/get-item'
export const preload = (id) => {
void getItem(id)
}
export default async function Item({ id }) {
const result = await getItem(id)
// ...
}
// app/item/[id]/page.js
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({ params: { id } }) {
// 开始加载 item 数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
cache
server-only
和预加载模式你可以将 cache
函数,preload
模式和 server-only
包一起使用,创建一个可在整个应用使用的数据请求工具函数。
// 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
。
恭喜你,完成了本篇内容的学习!
这一节我们介绍了请求数据的四种方式,重点介绍了服务端使用 fetch 的方式,这是因为 Next.js 拓展了原生的 fetch,增加了数据缓存和重新验证的逻辑。在 Next.js 中,为了提高性能,应该尽可能的使用缓存,但为了保证数据的时效性,也应该设置合理的重新验证逻辑。Next.js 推荐单独配置每个请求的缓存行为,这可以让你更精细化的控制缓存行为。
介绍完四种数据请求方式后,Next.js 提供了一些获取数据的建议和最佳实践,正是因为有了强大的缓存功能,所以在书写代码的时候可以就地获取数据,而不用担心相同请求多次发送造成的性能影响。