🙋 在 Next.js 中获取数据有多种方式。什么时候用 fetch?什么时候直接查数据库?如何避免请求瀑布?
数据获取方式#
1. Server Components 中的 fetch#
// Next.js 15.x
interface Post { id: number title: string body: string}
async function getPosts(): Promise<Post[]> { const res = await fetch('https://jsonplaceholder.typicode.com/posts')
if (!res.ok) { throw new Error('获取文章失败') }
return res.json()}
export default async function PostsPage() { const posts = await getPosts()
return ( <ul className="space-y-4"> {posts.slice(0, 10).map((post) => ( <li key={post.id}> <h2 className="font-bold">{post.title}</h2> <p className="text-gray-600">{post.body}</p> </li> ))} </ul> )}2. 直接访问数据库#
// Next.js 15.ximport { db } from '@/lib/db'
export default async function UsersPage() { // 直接使用 ORM 查询 const users = await db.user.findMany({ select: { id: true, name: true, email: true, }, orderBy: { createdAt: 'desc' }, take: 20, })
return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> )}3. 读取文件系统#
// Next.js 15.ximport { readdir, readFile } from 'fs/promises'import { join } from 'path'import matter from 'gray-matter'
async function getDocs() { const docsDir = join(process.cwd(), 'content', 'docs') const files = await readdir(docsDir)
const docs = await Promise.all( files .filter((file) => file.endsWith('.md')) .map(async (file) => { const content = await readFile(join(docsDir, file), 'utf-8') const { data, content: body } = matter(content) return { slug: file.replace('.md', ''), ...data, body } }) )
return docs}
export default async function DocsPage() { const docs = await getDocs() return <div>{/* 渲染文档列表 */}</div>}请求模式#
顺序请求(瀑布流)#
// ❌ 效率低:请求依次执行async function Page() { const user = await getUser() // 1秒 const posts = await getPosts() // 1秒 const comments = await getComments() // 1秒 // 总计:3秒
return <div>{/* ... */}</div>}并行请求#
// ✅ 高效:请求同时执行async function Page() { const [user, posts, comments] = await Promise.all([ getUser(), getPosts(), getComments(), ]) // 总计:~1秒(取决于最慢的请求)
return <div>{/* ... */}</div>}预加载模式#
import { cache } from 'react'
// 使用 cache 包装确保请求去重export const getUser = cache(async (id: string) => { const res = await fetch(`https://api.example.com/users/${id}`) return res.json()})
// 预加载函数export const preloadUser = (id: string) => { void getUser(id)}import { getUser, preloadUser } from '@/lib/data'import { UserProfile } from '@/components/UserProfile'
export default async function UserPage({ params,}: { params: Promise<{ id: string }>}) { const { id } = await params
// 提前开始获取数据 preloadUser(id)
return <UserProfile userId={id} />}import { getUser } from '@/lib/data'
export async function UserProfile({ userId }: { userId: string }) { // 复用已经开始的请求 const user = await getUser(userId)
return <div>{user.name}</div>}数据传递#
Props 传递#
// Next.js 15.ximport { StatsCard } from '@/components/StatsCard'
async function getStats() { const res = await fetch('https://api.example.com/stats') return res.json()}
export default async function DashboardPage() { const stats = await getStats()
return ( <div className="grid grid-cols-3 gap-4"> <StatsCard title="用户数" value={stats.users} /> <StatsCard title="订单数" value={stats.orders} /> <StatsCard title="收入" value={`¥${stats.revenue}`} /> </div> )}Server → Client 传递#
// app/products/page.tsx - Server Componentimport { ProductFilter } from '@/components/ProductFilter'
async function getProducts() { const res = await fetch('https://api.example.com/products') return res.json()}
async function getCategories() { const res = await fetch('https://api.example.com/categories') return res.json()}
export default async function ProductsPage() { const [products, categories] = await Promise.all([ getProducts(), getCategories(), ])
// 传递给 Client Component return <ProductFilter initialProducts={products} categories={categories} />}🔶 注意:传递的数据必须可序列化(不能传函数、类实例等)。
组件级数据获取#
// components/UserAvatar.tsx - Server Componentasync function getUser(id: string) { const res = await fetch(`https://api.example.com/users/${id}`) return res.json()}
export async function UserAvatar({ userId }: { userId: string }) { const user = await getUser(userId)
return ( <img src={user.avatar} alt={user.name} className="w-10 h-10 rounded-full" /> )}import { UserAvatar } from '@/components/UserAvatar'
export default function Page() { return ( <div> <UserAvatar userId="1" /> <UserAvatar userId="2" /> {/* 请求会自动去重 */} </div> )}请求去重#
自动去重#
Next.js 扩展了 fetch,相同请求自动去重:
// 以下两个调用实际上只发送一次请求async function getUser() { return fetch('https://api.example.com/user').then((r) => r.json())}
async function ComponentA() { const user = await getUser() return <div>{user.name}</div>}
async function ComponentB() { const user = await getUser() // 复用 ComponentA 的请求 return <div>{user.email}</div>}使用 React cache#
对于非 fetch 的数据获取,使用 cache:
import { cache } from 'react'import { db } from './db'
export const getUser = cache(async (id: string) => { return db.user.findUnique({ where: { id } })})错误处理#
try-catch#
async function getData() { try { const res = await fetch('https://api.example.com/data')
if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`) }
return res.json() } catch (error) { console.error('数据获取失败:', error) return null }}
export default async function Page() { const data = await getData()
if (!data) { return <div>数据加载失败,请稍后重试</div> }
return <div>{data.content}</div>}抛出错误#
async function getData() { const res = await fetch('https://api.example.com/data')
if (!res.ok) { throw new Error('获取数据失败') }
return res.json()}
export default async function Page() { const data = await getData() // 错误会被 error.tsx 捕获 return <div>{data.content}</div>}使用 notFound#
import { notFound } from 'next/navigation'
async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`)
if (res.status === 404) { notFound() }
if (!res.ok) { throw new Error('获取文章失败') }
return res.json()}实战示例#
博客列表与详情#
import { cache } from 'react'
export interface Post { slug: string title: string content: string publishedAt: string}
const API_URL = 'https://api.example.com'
export const getPosts = cache(async (): Promise<Post[]> => { const res = await fetch(`${API_URL}/posts`, { next: { revalidate: 60 }, }) return res.json()})
export const getPost = cache(async (slug: string): Promise<Post | null> => { const res = await fetch(`${API_URL}/posts/${slug}`, { next: { tags: [`post-${slug}`] }, })
if (!res.ok) return null return res.json()})import Link from 'next/link'import { getPosts } from '@/lib/posts'
export default async function BlogPage() { const posts = await getPosts()
return ( <div className="space-y-6"> <h1 className="text-3xl font-bold">博客</h1> <ul className="space-y-4"> {posts.map((post) => ( <li key={post.slug}> <Link href={`/blog/${post.slug}`} className="block p-4 border rounded hover:bg-gray-50" > <h2 className="font-semibold">{post.title}</h2> <time className="text-sm text-gray-500">{post.publishedAt}</time> </Link> </li> ))} </ul> </div> )}import { notFound } from 'next/navigation'import { getPost, getPosts } from '@/lib/posts'
export async function generateStaticParams() { const posts = await getPosts() return posts.map((post) => ({ slug: post.slug }))}
export default async function PostPage({ params,}: { params: Promise<{ slug: string }>}) { const { slug } = await params const post = await getPost(slug)
if (!post) { notFound() }
return ( <article className="prose max-w-none"> <h1>{post.title}</h1> <time>{post.publishedAt}</time> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> )}常见问题#
🤔 Q: fetch 和 axios 怎么选?
推荐使用原生 fetch,因为 Next.js 对其做了扩展(缓存、去重等)。
🤔 Q: 数据获取应该放在页面还是组件中?
- 页面级数据:放在页面组件中
- 组件专属数据:可以放在组件中
- 共享数据:抽取到独立函数,使用 cache 包装
🤔 Q: 如何处理加载状态?
使用 loading.tsx 或 Suspense 边界。
下一篇将深入缓存策略,掌握 fetch 缓存、数据缓存与路由缓存的配置。
-EOF-