在构建 Web 应用时,数据获取和渲染策略的选择直接影响着应用的性能、用户体验和 SEO。Next.js 提供了多种数据获取方法和渲染策略,让我们可以根据不同的场景选择最合适的方案。 在这一节课中,我们将深入探索这些方法,学习如何在服务器组件中获取数据,如何控制缓存行为,以及如何选择合适的渲染策略。

Next.js 提供了几种数据获取方式,每种方式都有其适用场景。最常用的是在服务器组件中直接使用 fetch API。Next.js 扩展了原生的 fetch API,添加了自动请求去重、缓存控制等功能。
让我们从一个简单的例子开始:
|// app/products/page.tsx export default async function ProductsPage() { const response = await fetch('https://api.example.com/products'); const products = await response.json(); return ( <div> <h1>产品列表</h1> <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> </div> ); }
这个例子展示了最基本的用法。我们在服务器组件中使用 fetch 来获取数据,然后渲染页面。Next.js 会自动处理请求的去重和缓存。
但要注意,这个例子中的 fetch 请求默认会被缓存。这意味着如果多个组件请求同一个 URL,Next.js 只会发送一次请求,并共享结果。这对于减少不必要的网络请求很有帮助,但有时候我们可能需要每次都获取最新数据。
Next.js 的 fetch API 默认会缓存请求结果。这意味着相同的请求在短时间内只会执行一次,后续的请求会直接使用缓存的结果。这个行为可以通过 cache 选项来控制。
让我们看一个不缓存的例子:
|// app/products/page.tsx export default async function ProductsPage() { const response = await fetch('https://api.example.com/products', { cache: 'no-store', // 不缓存,每次都获取最新数据 }); const products = await response.json(); return ( <div> <h1>产品列表</h1> <
通过设置 cache: 'no-store',我们告诉 Next.js 不要缓存这个请求。每次访问这个页面时,都会重新获取数据。这对于需要实时数据的场景很有用,比如股票价格、实时聊天等。
另一个选项是使用时间重新验证:
|// app/products/page.tsx export default async function ProductsPage() { const response = await fetch('https://api.example.com/products', { next: { revalidate: 3600 }, // 每 3600 秒(1小时)重新验证一次 }); const products = await response.json(); return ( <div> <h1>产品列表</h1>
revalidate 选项告诉 Next.js 在指定的时间间隔内使用缓存,超过这个时间后,如果有新的请求,会在后台重新获取数据并更新缓存。这被称为增量静态再生(ISR),我们稍后会详细讲解。
静态生成是在构建时生成 HTML 页面的方式。这种方式适用于内容不经常变化的页面,比如博客文章、产品详情页等。静态生成的页面可以直接从 CDN 提供,加载速度非常快。
在 Next.js App Router 中,默认情况下,所有使用 fetch 的页面都会在构建时静态生成。让我们看一个例子:
|// app/blog/[slug]/page.tsx async function getPost(slug: string) { const response = await fetch(`https://api.example.com/posts/${slug}`, { // 默认会缓存,在构建时获取 }); if (!response.ok) { throw new Error('Failed to fetch post'); } return response.json();
这个页面会在构建时生成。但这里有一个问题:我们不知道所有可能的 slug 值。为了解决这个问题,我们需要使用 generateStaticParams 函数。
|// app/blog/[slug]/page.tsx export async function generateStaticParams() { const response = await fetch('https://api.example.com/posts'); const posts = await response.json(); return posts.map((post: { slug: string }) => ({ slug: post.slug, })); } async
generateStaticParams 函数在构建时运行,返回所有需要静态生成的路由参数。Next.js 会为每个参数组合生成一个静态页面。
服务端渲染是在每次请求时在服务器端生成 HTML 的方式。这种方式适用于需要实时数据或个性化内容的页面。
要启用服务端渲染,我们需要在 fetch 中使用 cache: 'no-store',或者设置页面为动态:
|// app/dashboard/page.tsx export const dynamic = 'force-dynamic'; export default async function DashboardPage() { const user = await getCurrentUser(); const data = await fetchUserData(user.id, { cache: 'no-store', }); return ( <div> <h1>欢迎, {user.name}</
通过设置 export const dynamic = 'force-dynamic',我们告诉 Next.js 这个页面应该使用动态渲染,每次请求时都重新生成。
增量静态再生结合了静态生成和服务端渲染的优点。页面在构建时生成,但可以在后台定期更新,无需重新构建整个应用。
让我们看一个使用 ISR 的例子:
|// app/products/page.tsx export const revalidate = 3600; // 每 3600 秒重新验证 export default async function ProductsPage() { const response = await fetch('https://api.example.com/products', { next: { revalidate: 3600 }, }); const products = await response.json(); return ( <div
在这个例子中,我们设置了两个地方:页面级别的 revalidate 和 fetch 级别的 revalidate。页面级别的 revalidate 控制整个页面的重新验证间隔,fetch 级别的 revalidate 控制特定请求的重新验证间隔。
当用户访问这个页面时,如果缓存还在有效期内,Next.js 会直接返回缓存的页面。如果缓存已过期,Next.js 会在后台重新生成页面,同时仍然返回旧的缓存页面给用户。一旦新页面生成完成,缓存会被更新,下次访问就会使用新页面。
除了基于时间的重新验证,Next.js 还支持按需重新验证。这允许我们在数据更新时手动触发页面重新生成,而无需等待时间间隔。
要使用按需重新验证,我们需要创建一个 API 路由:
|// app/api/revalidate/route.ts import { revalidatePath } from 'next/cache'; import { NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const secret = request.nextUrl.searchParams.get('secret'); // 验证 secret,确保只有授权用户可以触发重新验证 if (secret !== process.env.REVALIDATE_SECRET) { return new
然后,我们可以在数据更新时调用这个 API:
|// 在内容管理系统中,当文章更新时 await fetch('https://your-site.com/api/revalidate?secret=your-secret&path=/blog/my-post');
这样,当内容更新时,我们可以立即触发页面重新生成,而不需要等待时间间隔。
在使用 fetch 获取数据时,有几个最佳实践需要注意。
首先,尽量在需要数据的组件中直接获取数据,而不是通过 props 层层传递。这样可以减少不必要的数据传递,让组件更加独立。
|// 好的做法:在需要数据的组件中直接获取 // app/products/[id]/page.tsx export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const product = await fetchProduct(id); return <ProductDetails
其次,对于需要多个数据源的页面,可以使用并行数据获取:
|// app/product/[id]/page.tsx export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; // 并行获取多个数据源 const [product, reviews, relatedProducts] = await
使用 Promise.all 可以并行执行多个异步操作,而不是串行执行,这可以显著减少页面加载时间。
第三,对于可能失败的数据获取,要添加错误处理:
|// app/products/[id]/page.tsx export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; try { const product = await fetchProduct(id); return <
如果数据获取失败,我们可以使用 notFound() 函数来显示 404 页面,或者使用 error.tsx 来处理错误。
有时候,我们可能需要在同一个渲染过程中多次使用相同的数据。为了避免重复获取,我们可以使用 React 的 cache 函数:
|import { cache } from 'react'; const getProduct = cache(async (id: string) => { const response = await fetch(`https://api.example.com/products/${id}`); return response.json(); }); export default async function ProductPage({ params
cache 函数确保在同一个渲染过程中,相同的参数只会执行一次函数调用,后续调用会直接返回缓存的结果。
Next.js 支持流式渲染,这允许我们在数据准备好时逐步发送 HTML 给客户端,而不是等待所有数据都准备好。这可以显著提升首屏加载速度。xw
流式渲染是自动启用的,当我们使用 Suspense 边界时:
|// app/products/page.tsx import { Suspense } from 'react'; async function ProductList() { const products = await fetchProducts(); return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </
在这个例子中,ProductList 组件被包裹在 Suspense 中。当 ProductList 正在加载数据时,Next.js 会先发送 ProductListSkeleton 的 HTML 给客户端。一旦 ProductList 的数据准备好,Next.js 会发送实际的列表内容,替换骨架屏。
在这一节中,我们探索了 Next.js 的数据获取和渲染策略。我们学习了如何使用 fetch API 获取数据,如何控制缓存行为,了解了静态生成、服务端渲染和增量静态再生的区别和适用场景。
静态生成适合内容不经常变化的页面,服务端渲染适合需要实时数据的页面,增量静态再生则提供了两者之间的平衡。
在下一节课,我们将学习路由和导航,了解如何在 Next.js 应用中实现页面间的导航,如何使用动态路由,以及如何处理路由参数和查询字符串。