Next.js 面试题详细答案 - Q7
Q7: 在 App Router 中,推荐的数据获取方法是什么?与 Pages Router 中的 getServerSideProps/getStaticProps 有何不同?
App Router 数据获取方法
1. Server Components 直接获取数据
// app/blog/page.js - 直接在组件中获取数据
async function BlogPage() {
const posts = await fetch('https://api.example.com/posts')
const data = await posts.json()
return (
<div>
<h1>博客列表</h1>
{data.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
// app/blog/[slug]/page.js - 动态路由数据获取
async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
const data = await post.json()
return (
<article>
<h1>{data.title}</h1>
<div>{data.content}</div>
</article>
)
}
2. 缓存策略控制
// 强制缓存 - 静态数据
async function StaticData() {
const data = await fetch('https://api.example.com/static-data', {
cache: 'force-cache',
})
return <div>{data}</div>
}
// 不缓存 - 动态数据
async function DynamicData() {
const data = await fetch('https://api.example.com/dynamic-data', {
cache: 'no-store',
})
return <div>{data}</div>
}
// ISR - 定期重新验证
async function ISRData() {
const data = await fetch('https://api.example.com/isr-data', {
next: { revalidate: 3600 }, // 1小时
})
return <div>{data}</div>
}
// 标签缓存 - 按需重新验证
async function TaggedData() {
const data = await fetch('https://api.example.com/tagged-data', {
next: { tags: ['posts'] },
})
return <div>{data}</div>
}
3. 数据获取最佳实践
// 错误处理
async function BlogPost({ params }) {
try {
const response = await fetch(`https://api.example.com/posts/${params.slug}`)
if (!response.ok) {
throw new Error('Failed to fetch post')
}
const post = await response.json()
return <article>{post.content}</article>
} catch (error) {
return <div>加载失败: {error.message}</div>
}
}
// 并行数据获取
async function Dashboard() {
const [users, posts, analytics] = await Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/analytics'),
])
const [usersData, postsData, analyticsData] = await Promise.all([
users.json(),
posts.json(),
analytics.json(),
])
return (
<div>
<UserList users={usersData} />
<PostList posts={postsData} />
<Analytics data={analyticsData} />
</div>
)
}
与 Pages Router 的对比
Pages Router 数据获取
// pages/blog.js - getServerSideProps
export async function getServerSideProps(context) {
const { params, query, req, res } = context
const posts = await fetch('https://api.example.com/posts')
const data = await posts.json()
return {
props: {
posts: data,
},
}
}
export default function BlogPage({ posts }) {
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
// pages/blog/[slug].js - getStaticProps
export async function getStaticProps({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
const data = await post.json()
return {
props: {
post: data,
},
revalidate: 3600, // ISR
}
}
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts')
const data = await posts.json()
const paths = data.map((post) => ({
params: { slug: post.slug },
}))
return {
paths,
fallback: 'blocking',
}
}
核心差异对比
| 特性 | Pages Router | App Router |
|---|---|---|
| 数据获取位置 | 页面级别 | 组件级别 |
| 方法 | getServerSideProps/getStaticProps | 直接 fetch |
| 缓存控制 | 简单 | 精细化 |
| 错误处理 | 页面级 | 组件级 |
| 并行获取 | 复杂 | 简单 |
| 类型安全 | 需要类型定义 | 原生支持 |
实际迁移示例
从 Pages Router 迁移到 App Router
// 迁移前 - Pages Router
// pages/products/[id].js
export async function getServerSideProps({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`)
const data = await product.json()
return {
props: { product: data },
}
}
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
)
}
// 迁移后 - App Router
// app/products/[id]/page.js
async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`)
const data = await product.json()
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
)
}
export default ProductPage
高级数据获取模式
1. 数据获取 Hook 模式
// lib/data.js - 数据获取函数
export async function getPosts() {
const response = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
})
return response.json()
}
export async function getPost(slug) {
const response = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
})
return response.json()
}
// app/blog/page.js - 使用数据获取函数
import { getPosts } from '@/lib/data'
export default async function BlogPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
2. 条件数据获取
async function ConditionalData({ userId, showAnalytics }) {
const user = await fetch(`https://api.example.com/users/${userId}`)
const userData = await user.json()
let analytics = null
if (showAnalytics) {
const analyticsResponse = await fetch(`https://api.example.com/analytics/${userId}`)
analytics = await analyticsResponse.json()
}
return (
<div>
<h1>{userData.name}</h1>
{analytics && <Analytics data={analytics} />}
</div>
)
}
3. 数据预加载
// 预加载数据
async function PreloadData() {
const postsPromise = fetch('https://api.example.com/posts')
const usersPromise = fetch('https://api.example.com/users')
return {
posts: postsPromise,
users: usersPromise,
}
}
// 使用预加载数据
async function Dashboard() {
const { posts, users } = await PreloadData()
const [postsData, usersData] = await Promise.all([
posts.then(res => res.json()),
users.then(res => res.json()),
])
return (
<div>
<PostList posts={postsData} />
<UserList users={usersData} />
</div>
)
}
性能优化策略
1. 请求去重
// Next.js 自动去重相同请求
async function UserProfile({ userId }) {
const user = await fetch(`https://api.example.com/users/${userId}`)
return <div>{user.name}</div>
}
// 多个组件使用相同请求时,只会发送一个请求
function UserPage({ userId }) {
return (
<div>
<UserProfile userId={userId} />
<UserStats userId={userId} />
<UserPosts userId={userId} />
</div>
)
}
2. 流式传输
import { Suspense } from 'react'
async function BlogPage() {
return (
<div>
<h1>博客</h1>
<Suspense fallback={<div>加载文章...</div>}>
<PostList />
</Suspense>
<Suspense fallback={<div>加载推荐...</div>}>
<Recommendations />
</Suspense>
</div>
)
}
async function PostList() {
const posts = await fetch('https://api.example.com/posts')
const data = await posts.json()
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
总结
App Router 的数据获取方法相比 Pages Router 有以下优势:
- 更简单:直接在组件中获取数据,无需额外的函数
- 更灵活:组件级别的数据获取,支持更细粒度的控制
- 更强大:内置缓存策略,支持 ISR 和标签缓存
- 更类型安全:原生 TypeScript 支持
- 更高效:自动请求去重和并行处理
这些改进让数据获取变得更加直观和高效,同时保持了强大的缓存和性能优化能力。