4.1 Server Component 数据获取
在 App Router 中,数据获取的推荐方式是直接在 Server Component 中使用 async/await。这消除了 Pages Router 中需要 getServerSideProps 和 getStaticProps 特殊函数的复杂性,让数据获取代码与组件 UI 紧密结合,提升了代码的可读性和维护性。
服务端数据获取的优势在于:数据库密钥、API Token 等敏感信息完全在服务端处理,永远不会暴露给客户端;同时可以直接查询数据库而无需额外的网络请求往返,减少了延迟。
// app/posts/page.tsx — 直接 async 获取数据
async function getPosts() {
// 这段代码只在服务端运行
const res = await fetch('https://api.example.com/posts')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts() // 直接 await,无 useEffect
return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
TSX
4.2 Next.js 15 的缓存语义变化
Next.js 15 对缓存行为做了重要调整:fetch 请求默认不再缓存(从 force-cache 改为 no-store)。这意味着每次请求都会重新获取数据,行为更接近开发者的直觉预期。如果需要缓存,需要显式声明。
- no-store 不缓存,每次请求都重新获取。Next.js 15 的默认值。适合实时数据(库存、价格等)。
- force-cache 永久缓存直到手动失效,等同于静态生成(SSG)。适合极少变化的数据(配置、内容)。
- revalidate: N N 秒后重新验证缓存(ISR:增量静态再生成)。平衡了性能与数据新鲜度。
- revalidatePath() 手动使某个路径的缓存失效,通常在数据变更后调用(如 Server Action 中)。
-
revalidateTag()
按标签批量使缓存失效,配合 fetch 的
next.tags选项使用。
// 不同的缓存策略
// 1. 不缓存(Next.js 15 默认)
const data = await fetch(url, { cache: 'no-store' })
// 2. 永久缓存(静态数据)
const config = await fetch(url, { cache: 'force-cache' })
// 3. 按时间重新验证(ISR,60秒)
const posts = await fetch(url, {
next: { revalidate: 60 }
})
// 4. 按标签缓存(可手动批量失效)
const posts = await fetch(url, {
next: { tags: ['posts'] }
})
// 在 Server Action 中手动失效
'use server'
import { revalidateTag, revalidatePath } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
await db.post.update(...)
revalidateTag('posts') // 使所有 posts 标签的缓存失效
revalidatePath('/blog') // 使 /blog 页面的缓存失效
}
TS
4.3 unstable_cache — 缓存任意异步函数
unstable_cache 是 Next.js 提供的服务端缓存 API,可以缓存任意异步函数的返回值,不仅限于 fetch 请求。当使用 ORM(如 Prisma)直接查询数据库时,这个 API 尤为重要。
命名前缀 "unstable_"虽然带有 "unstable" 前缀,但此 API 已在生产环境广泛使用,前缀表示 API 签名可能在未来版本变化。Next.js 15 同时引入了更完善的 use cache 实验性指令作为替代方案。
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'
// 缓存数据库查询结果
const getCachedUser = unstable_cache(
async (id: string) => {
// 这个函数的返回值会被缓存
return db.user.findUnique({ where: { id } })
},
['user'], // 缓存键前缀(数组)
{
revalidate: 3600, // 1小时后失效
tags: ['users', `user-${id}`], // 缓存标签
}
)
// 在 Server Component 中使用
export default async function UserProfile({ params }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user?.name}</div>
}
TS
4.4 loading.tsx 与 Suspense 边界
Next.js 的 loading.tsx 文件是对 React Suspense 的约定式封装。当 page.tsx 中的异步数据正在加载时,Next.js 会自动将 loading.tsx 的内容显示给用户,无需手动编写 <Suspense>。
这背后的机制是流式传输(Streaming):服务器会先发送页面的静态部分(布局、导航)的 HTML,然后在数据就绪后流式发送动态内容,替换掉 loading UI。用户感受到的是"瞬间看到页面框架,然后内容逐步加载"。
// app/dashboard/loading.tsx — 自动 Suspense fallback
export default function DashboardLoading() {
return (
<div className="loading-skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
</div>
)
}
TSX
手动使用 Suspense 实现更细粒度的加载状态
loading.tsx 会将整个页面放入单一 Suspense 边界。如果需要页面不同部分独立加载,应手动使用 <Suspense> 包裹各个数据依赖组件。
import { Suspense } from 'react'
import { PostList, PostListSkeleton } from './components'
import { RecommendedPosts } from './recommendations'
export default function BlogPage() {
return (
<div>
<h1>博客</h1>
{/* 文章列表独立加载 */}
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
{/* 推荐文章独立加载,不阻塞主列表 */}
<Suspense fallback={<p>加载推荐中...</p>}>
<RecommendedPosts />
</Suspense>
</div>
)
}
// PostList 内部是 async Server Component
async function PostList() {
const posts = await fetchPosts() // 慢速请求
return <ul>{posts.map(...)}</ul>
}
TSX
4.5 并行数据获取与 Promise.all
当页面需要多个独立的数据源时,应该并行发起请求,而不是串行等待。使用 Promise.all 或 Promise.allSettled 可以同时发起多个请求,总时间等于最慢请求的时间,而非所有请求时间之和。
// ❌ 串行获取 — 总时间 = 200ms + 300ms = 500ms
async function BadPage() {
const user = await fetchUser() // 200ms
const posts = await fetchPosts() // 300ms(等 user 完成才开始)
return /* ... */
}
// ✅ 并行获取 — 总时间 ≈ max(200ms, 300ms) = 300ms
async function GoodPage() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
])
return /* ... */
}
// ✅ 不需要等 posts 再渲染 user 时,用"预加载"模式
async function BestPage() {
// 立即发起请求,不 await(让它在后台进行)
const postsPromise = fetchPosts()
// await user(很快)
const user = await fetchUser()
// posts 已经在获取中,等待结果
const posts = await postsPromise
return /* ... */
}
TSX
4.6 路由段配置 — 控制渲染策略
每个路由段(layout.tsx 或 page.tsx)可以通过导出特殊的配置变量来控制其渲染和缓存行为,这些配置会影响整个路由段的构建时策略。
// app/dashboard/page.tsx
// 强制动态渲染(相当于旧版 getServerSideProps)
export const dynamic = 'force-dynamic'
// 强制静态渲染(相当于旧版 getStaticProps)
// export const dynamic = 'force-static'
// 路由段级别的 ISR(秒)
export const revalidate = 3600
// 使用 Edge Runtime(减少冷启动,限制 Node.js API 使用)
// export const runtime = 'edge'
// 动态路由是否预渲染全部可能的值
export const dynamicParams = false
export default async function Page() {
return <div>内容</div>
}
TSX
动态函数触发器使用 cookies()、headers()、searchParams(在 page.tsx 中)或 noStore() 会自动将该路由段标记为动态渲染,无需显式设置 dynamic = 'force-dynamic'。这是 Next.js 的"动态渲染自动检测"机制。