Next.js第六课 - 数据获取
上节我们学习了服务端组件和客户端组件的区别,本节来深入了解 Next.js 中的数据获取。Next.js 提供了灵活且强大的数据获取方式,掌握好这些知识能让你构建出性能优异的应用。
数据获取概述
在 Next.js 中,有多种数据获取方式:
- 静态生成(SSG)- 构建时生成 HTML
- 服务器端渲染(SSR)- 每次请求时生成 HTML
- 增量静态再生成(ISR)- 定期重新生成静态页面
- 客户端数据获取 - 在浏览器中获取数据
选择哪种方式取决于你的数据特性:是否经常变化、是否需要 SEO、用户是否需要看到最新数据等。
服务端组件数据获取
在服务端组件中,你可以直接使用 fetch 或任何数据获取库,这相比传统的 React 方式要简单很多。
基本数据获取
最简单的方式就是直接在组件中使用 async/await:
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
if (!res.ok) {
throw new Error('获取文章失败')
}
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>博客文章</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
)
}
直接访问数据库
服务端组件可以直接访问数据库,不需要创建 API 层:
// app/users/page.tsx
import { db } from '@/lib/db'
export default async function UsersPage() {
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
})
return (
<div>
<h1>用户列表</h1>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
错误处理
处理数据获取中的错误是很重要的:
// app/products/[id]/page.tsx
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
let product
try {
const res = await fetch(`https://api.example.com/products/${params.id}`)
if (!res.ok) {
if (res.status === 404) {
notFound()
}
throw new Error('获取产品失败')
}
product = await res.json()
} catch (error) {
return <div>加载产品时出错</div>
}
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
)
}
缓存和重新验证
Next.js 的缓存系统非常强大,理解它能让你的应用性能提升很多。
默认缓存行为
Next.js 默认会缓存 fetch 请求,这意味着相同的数据请求会被缓存起来,避免重复获取:
// 默认:自动缓存
export default async function Page() {
const res = await fetch('https://api.example.com/data')
const data = await res.json()
return <div>{/* ... */}</div>
}
禁用缓存
如果数据需要实时更新,可以禁用缓存:
// 不缓存:每次都获取新数据
export default async function Page() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
const data = await res.json()
return <div>{/* ... */}</div>
}
设置重新验证时间
最常用的方式是设置缓存时间:
// 缓存 10 秒后重新验证
export default async function Page() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 10 },
})
const data = await res.json()
return <div>{/* ... */}</div>
}
按标签重新验证
给数据加上标签,可以在数据更新时手动刷新缓存:
// 获取时添加标签
export default async function Page() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})
const posts = await res.json()
return <div>{/* ... */}</div>
}
// 在 API 路由中手动重新验证
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const tag = await request.json()
revalidateTag(tag)
return Response.json({ revalidated: true })
}
渲染策略
静态渲染(默认)
静态渲染意味着页面在构建时就生成好了 HTML:
// app/blog/page.tsx
// 构建时生成静态 HTML
export default async function BlogPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
动态渲染
每次请求都重新渲染:
// app/dashboard/page.tsx
// 每次请求都渲染
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
const user = await getCurrentUser()
return <div>欢迎,{user.name}</div>
}
增量静态再生成(ISR)
结合静态和动态的优点:
// app/products/page.tsx
// 每 60 秒重新生成页面
export const revalidate = 60
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
并行数据获取
当需要获取多个独立的数据源时,应该并行获取以提高性能:
// app/dashboard/page.tsx
export default async function DashboardPage() {
// 并行获取数据
const [user, posts, analytics] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchAnalytics(),
])
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<AnalyticsChart data={analytics} />
</div>
)
}
客户端数据获取
虽然服务端数据获取更推荐,但有时候也需要在客户端获取数据:
使用 useEffect
传统的方式:
'use client'
import { useState, useEffect } from 'react'
export default function ClientDataFetching() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchData() {
try {
const res = await fetch('https://api.example.com/data')
const json = await res.json()
setData(json)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return <div>{/* 渲染数据 */}</div>
}
使用 SWR
SWR 是一个很流行的数据获取库:
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((res) => res.json())
export default function Profile() {
const { data, error, isLoading } = useSWR(
'https://api.example.com/user',
fetcher
)
if (error) return <div>加载失败</div>
if (isLoading) return <div>加载中...</div>
return <div>你好,{data.name}</div>
}
加载状态
Next.js 提供了优雅的加载状态处理方式:
loading.tsx 文件
创建 loading.tsx 文件会自动显示加载状态:
// app/posts/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 w-1/2 mb-4"></div>
<div className="h-4 bg-gray-200 w-5/6"></div>
</div>
)
}
实用建议
这里分享几个在日常开发中特别实用的数据获取技巧。
优先使用服务端组件
实际开发中,我发现服务端数据获取不仅性能更好,代码也更简洁:
// 推荐这样做 - 服务端组件数据获取
export default async function Page() {
const data = await fetchData()
return <div>{data.title}</div>
}
// 除非有特殊需求,否则避免客户端数据获取
'use client'
export default function Page() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData)
}, [])
return <div>{data?.title}</div>
}
并行获取数据
这个小技巧特别有用——如果多个数据源是独立的,应该并行获取来提升性能:
// 推荐这样做 - 并行获取,速度更快
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
])
// 避免这种情况 - 串行获取会拖慢速度
const user = await fetchUser()
const posts = await fetchPosts() // 要等 user 完成才开始
使用适当的缓存策略
根据数据特性选择缓存策略,这个在实际项目中特别重要:
// 静态内容可以长时间缓存
const posts = await fetch('https://api.com/posts', {
next: { revalidate: 3600 },
})
// 实时数据建议不缓存
const stockPrices = await fetch('https://api.com/stocks', {
cache: 'no-store',
})
总结
本节我们详细学习了 Next.js 的数据获取机制,包括服务端和客户端获取、缓存策略、渲染策略等。掌握好这些知识,你就能根据不同的场景选择最合适的数据获取方式,构建出高性能的应用。
如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。