数据获取、缓存、重新校验
服务端使用 fetch
获取数据
Next.js 扩展了原生的 fetch
Web API ,允许对缓存及重新校验行为进行配置;React 也扩展了 fetch
方法,当渲染组件树时 React 会自动记忆其中的 fetch
请求。
但是 Route Handler 不会记忆 fetch 请求,因为它不归于 React 。
可以在服务端组件、Route Handler 、Server Action 中使用 fetch
:
async function getData() {
const res = await fetch('https://api.example.com/...');
if (!res.ok) 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
请求结果存在服务端:
// 'force-cache' 是默认的,可以省略
fetch('https://...', { cache: 'force-cache' })
POST
请求也会被缓存,但在 Route Handler 中不会被缓存。
重新校验数据
有两种重新校验缓存数据的方式:基于时间的重新校验 和 按需进行重新校验 。
对于一些变更不频繁或者新鲜度并不是很重要的数据,可以设置一个合理的时间间隔来自动进行重新校验:
fetch('https://...', { next: { revalidate: 3600 } })
或者导出一个路由片段配置:
export const revalidate = 3600
如果在静态渲染路由上有多个 fetch
请求,且每个请求都有不同的重新校验频率,会取其中的最小时间。对于动态渲染路由,每个 fetch
请求的重新校验都是相互独立的。
错误处理
如果在重新校验的过程中抛出了错误,则会使用缓存临时替代。在下一次请求中,Next.js 仍会尝试重新校验数据。
不使用缓存
以下方式均可让 fetch
不做数据缓存处理:
- 在
fetch
中加上cache: 'no-store'
; - 在
fetch
中加上revalidate: 0
; - 处于 POST 方法的 Router Handler 内的
fetch
; - 在使用了
headers
或cookies
之后的fetch
; - 使用了
const dynamic = 'force-dynamic'
的路由片段配置; - 通过路由片段配置
fetchCache
设置为默认跳过缓存; - 使用了
Authorization
或Cookie
标头的请求,且在组件树上有一个未缓存的请求位于其之上。
通常来讲,如果是单独的 fetch
请求,可以直接用 cache
选项来控制缓存;如果在路由片段中有多个 fetch
请求,可以通过路由片段配置来设置所有的请求的默认缓存行为(不过更建议对每一个 fetch
请求都单独地进行缓存行为的设置)。
用第三方库请求数据
TODO
客户端调用 Route Handler
在客户端可以调用 Route Handler 。这对一些场景比较有用,比如当你不想在响应中暴露一些敏感信息(比如 API token)。
修改数据
TODO
最佳实践
服务端获取数据
通常更推荐在服务端获取数据(如果可以的话),可以:
- 直接访问后端资源(比如数据库);
- 防止一些敏感信息暴露给客户端,比如 access token 或者 API key 等;
- 使数据请求和渲染处于同一环境中,减少客户端和服务端的沟通成本;
- 在客户端上使用单次往返执行多个数据获取,而不是直接发出多个独立的请求。
- 减少“客户端-服务端”的瀑布流;
- 服务端可能更接近你的数据资源,可以缩短请求时间。
可通过服务端组件、Route Handler 、Server Action 来在服务端获取数据。
在需要的地方获取数据
TODO
如果在组件树上有多个组件需要使用同一数据,可以直接用 fetch
或 React cache
来获取数据,两者都会自动做 记忆化处理 ,不会导致重复的请求。
流式渲染
对于有多个请求的页面,请求这些接口的模式有两种:依次请求和同时请求。
依次请求
该模式的场景通常为一个请求为另一个请求的依赖。在这种场景,对于有依赖的接口可以用 Suspense
进行等待,确保用户能先触达先前已请求数据相关的页面内容:
async function Playlists({ artistID }) {
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 } }) {
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
);
}
同时请求
一个组件中如果有不相依赖的接口,通常会同时请求这些接口:
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)
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
但同时请求可能会让用户等待的时间过长。如果可以的话可以根据接口拆分页面,并分别用 Suspense
来各自等待数据请求。
预加载数据
TODO
避免将敏感信息暴露给用户
TODO