数据获取、缓存、重新校验
服务端使用 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