跳到內容

資料抓取與快取

範例

本指南將帶您瞭解 Next.js 中資料抓取和快取的基本知識,並提供實務範例和最佳實務。

以下是在 Next.js 中進行資料抓取的一個最小範例

app/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}>{post.title}</li>
      ))}
    </ul>
  )
}

此範例示範了在非同步 React 伺服器組件中使用 fetch API 進行基本伺服器端資料抓取。

參考

範例

使用 fetch API 在伺服器上抓取資料

此組件將抓取並顯示部落格文章列表。預設情況下,來自 fetch 的回應不會被快取。

app/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}>{post.title}</li>
      ))}
    </ul>
  )
}

如果您在此路由中的其他任何地方未使用任何動態 API,則會在 next build 期間預先渲染為靜態頁面。然後可以使用漸進式靜態重新產生來更新資料。

若要防止頁面預先渲染,您可以將以下內容新增至您的檔案

export const dynamic = 'force-dynamic'

但是,您通常會使用諸如 cookiesheaders 或從頁面 props 讀取傳入的 searchParams 等函式,這些函式會自動使頁面動態渲染。在這種情況下,您不需要明確使用 force-dynamic

使用 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>
  )
}

如果您在此路由中的其他任何地方未使用任何動態 API,則會在 next build 期間預先渲染為靜態頁面。然後可以使用漸進式靜態重新產生來更新資料。

若要防止頁面預先渲染,您可以將以下內容新增至您的檔案

export const dynamic = 'force-dynamic'

但是,您通常會使用諸如 cookiesheaders 或從頁面 props 讀取傳入的 searchParams 等函式,這些函式會自動使頁面動態渲染。在這種情況下,您不需要明確使用 force-dynamic

在用戶端抓取資料

我們建議首先嘗試在伺服器端抓取資料。

但是,在某些情況下,用戶端資料抓取仍然有其意義。在這些情況下,您可以手動在 useEffect 中呼叫 fetch(不建議),或依賴社群中流行的 React 函式庫(例如 SWRReact Query)進行用戶端抓取。

app/page.tsx
'use client'
 
import { useState, useEffect } from 'react'
 
export function Posts() {
  const [posts, setPosts] = useState(null)
 
  useEffect(() => {
    async function fetchPosts() {
      const res = await fetch('https://api.vercel.app/blog')
      const 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>
  )
}

此範例會快取資料庫查詢結果 1 小時(3600 秒)。它還新增了快取標籤 posts,然後可以使用漸進式靜態重新產生使該標籤失效。

跨多個函式重複使用資料

Next.js 使用諸如 generateMetadatagenerateStaticParams 等 API,您需要在 page 中使用相同的抓取資料。

如果您使用 fetch,則可以透過新增 cache: 'force-cache'記憶化請求。這表示您可以安全地使用相同的選項呼叫相同的 URL,並且只會發出一個請求。

要知道的好

  • 在 Next.js 的先前版本中,使用 fetch 會將預設 cache 值設定為 force-cache。這在版本 15 中已變更為預設值 cache: no-store
app/blog/[id]/page.tsx
import { notFound } from 'next/navigation'
 
interface Post {
  id: string
  title: string
  content: string
}
 
async function getPost(id: string) {
  const res = await fetch(`https://api.vercel.app/blog/${id}`, {
    cache: 'force-cache',
  })
  const post: Post = await res.json()
  if (!post) notFound()
  return post
}
 
export async function generateStaticParams() {
  const posts = await fetch('https://api.vercel.app/blog', {
    cache: 'force-cache',
  }).then((res) => res.json())
 
  return posts.map((post: Post) => ({
    id: String(post.id),
  }))
}
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return {
    title: post.title,
  }
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(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
})

重新驗證快取資料

深入瞭解如何使用漸進式靜態重新產生來重新驗證快取資料。

模式

平行和循序資料抓取

在組件內部抓取資料時,您需要注意兩種資料抓取模式:平行和循序。

Sequential and Parallel Data Fetching
  • 循序:組件樹狀結構中的請求彼此依賴。這可能會導致更長的載入時間。
  • 平行:路由中的請求會主動啟動,並同時載入資料。這減少了載入資料所需的總時間。

循序資料抓取

如果您有巢狀組件,並且每個組件都抓取自己的資料,則如果這些資料請求未記憶化,則資料抓取將循序發生。

在某些情況下,您可能需要這種模式,因為一個抓取依賴於另一個抓取的結果。例如,Playlists 組件只有在 Artist 組件完成資料抓取後才會開始抓取資料,因為 Playlists 依賴於 artistID prop

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>
  )
}

您可以使用loading.js(用於路由區段)或React <Suspense>(用於巢狀組件)來顯示即時載入狀態,同時 React 串流結果。

這將防止整個路由被資料請求封鎖,並且使用者將能夠與頁面中已準備好的部分互動。

平行資料抓取

預設情況下,版面配置和頁面區段會平行渲染。這表示請求將平行啟動。

但是,由於 async/await 的性質,在同一區段或組件內等待的請求將封鎖其下方的任何請求。

若要平行抓取資料,您可以透過在組件外部定義請求來主動啟動請求,這些組件使用資料。這透過平行啟動兩個請求來節省時間,但是,在兩個 Promise 都解析之前,使用者不會看到渲染的結果。

在以下範例中,getArtistgetAlbums 函式在 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,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  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 Boundary來分解渲染工作,並儘快顯示部分結果。

預先載入資料

防止瀑布流的另一種方法是使用預先載入模式,方法是建立一個實用函式,您可以在封鎖請求之上主動呼叫該函式。例如,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)
  // ...
}
app/item/[id]/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
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
}

要知道的好: 「預先載入」函式也可以有任何名稱,因為它是一種模式,而不是 API。

將 React cacheserver-only 與 Preload Pattern 搭配使用

您可以結合 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 匯出,讓它們可以控制何時抓取項目的資料。

要知道的好

  • 我們建議使用server-only 套件,以確保伺服器資料抓取函式永遠不會在用戶端上使用。

防止敏感資料暴露給用戶端

我們建議使用 React 的 taint API,taintObjectReferencetaintUniqueValue,以防止整個物件實例或敏感值傳遞到用戶端。

若要在您的應用程式中啟用 tainting,請將 Next.js Config experimental.taint 選項設定為 true

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

然後將您要 taint 的物件或值傳遞給 experimental_taintObjectReferenceexperimental_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/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
    />
  )
}