Next.js 面试题详细答案 - Q8
Q8: 如何在 Server Component 中获取数据?请写出一个异步 Server Component 的例子。
Server Component 数据获取基础
1. 基本异步 Server Component
// app/blog/page.js
async function BlogPage() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await response.json()
return (
<div>
<h1>博客文章</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
)
}
export default BlogPage
2. 带错误处理的异步组件
// app/users/page.js
async function UsersPage() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
next: { revalidate: 3600 }, // 1小时缓存
})
if (!response.ok) {
throw new Error('Failed to fetch users')
}
const users = await response.json()
return (
<div>
<h1>用户列表</h1>
<div className="grid">
{users.map((user) => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>{user.phone}</p>
</div>
))}
</div>
</div>
)
} catch (error) {
return (
<div>
<h1>加载失败</h1>
<p>错误: {error.message}</p>
</div>
)
}
}
export default UsersPage
复杂数据获取示例
1. 并行数据获取
// app/dashboard/page.js
async function Dashboard() {
// 并行获取多个数据源
const [postsResponse, usersResponse, analyticsResponse] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts?_limit=5'),
fetch('https://jsonplaceholder.typicode.com/users?_limit=5'),
fetch('https://jsonplaceholder.typicode.com/albums?_limit=5'),
])
const [posts, users, analytics] = await Promise.all([
postsResponse.json(),
usersResponse.json(),
analyticsResponse.json(),
])
return (
<div className="dashboard">
<h1>仪表盘</h1>
<section>
<h2>最新文章</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
</section>
<section>
<h2>用户统计</h2>
<div className="stats">
<div>总用户数: {users.length}</div>
<div>总文章数: {posts.length}</div>
<div>总相册数: {analytics.length}</div>
</div>
</section>
</div>
)
}
export default Dashboard
2. 条件数据获取
// app/profile/[id]/page.js
async function UserProfile({ params }) {
const userId = params.id
// 获取用户基本信息
const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
const user = await userResponse.json()
// 根据用户类型获取不同数据
let additionalData = null
if (user.id <= 3) {
// 管理员用户获取更多数据
const [postsResponse, albumsResponse] = await Promise.all([
fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`),
fetch(`https://jsonplaceholder.typicode.com/albums?userId=${userId}`),
])
const [posts, albums] = await Promise.all([
postsResponse.json(),
albumsResponse.json(),
])
additionalData = { posts, albums }
}
return (
<div className="profile">
<h1>{user.name}</h1>
<p>邮箱: {user.email}</p>
<p>电话: {user.phone}</p>
<p>网站: {user.website}</p>
{additionalData && (
<div>
<h2>用户内容</h2>
<div>
<h3>文章 ({additionalData.posts.length})</h3>
<ul>
{additionalData.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
<div>
<h3>相册 ({additionalData.albums.length})</h3>
<ul>
{additionalData.albums.map((album) => (
<li key={album.id}>{album.title}</li>
))}
</ul>
</div>
</div>
)}
</div>
)
}
export default UserProfile
数据库直接访问示例
1. 使用 Prisma
// app/products/page.js
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function ProductsPage() {
const products = await prisma.product.findMany({
include: {
category: true,
reviews: {
take: 3,
orderBy: { rating: 'desc' },
},
},
})
return (
<div>
<h1>产品列表</h1>
<div className="products-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>价格: ${product.price}</p>
<p>分类: {product.category.name}</p>
<div>
<h4>最新评价:</h4>
{product.reviews.map((review) => (
<div key={review.id}>
<p>评分: {review.rating}/5</p>
<p>{review.comment}</p>
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}
export default ProductsPage
2. 使用 MongoDB
// app/blog/[slug]/page.js
import { MongoClient } from 'mongodb'
async function BlogPost({ params }) {
const client = new MongoClient(process.env.MONGODB_URI)
try {
await client.connect()
const db = client.db('blog')
const collection = db.collection('posts')
const post = await collection.findOne({ slug: params.slug })
if (!post) {
return <div>文章未找到</div>
}
return (
<article>
<h1>{post.title}</h1>
<div className="meta">
<span>作者: {post.author}</span>
<span>发布时间: {new Date(post.publishedAt).toLocaleDateString()}</span>
</div>
<div className="content">
{post.content}
</div>
</article>
)
} finally {
await client.close()
}
}
export default BlogPost
文件系统访问示例
1. 读取 Markdown 文件
// app/blog/[slug]/page.js
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
async function BlogPost({ params }) {
const filePath = path.join(process.cwd(), 'content', 'blog', `${params.slug}.md`)
try {
const fileContent = fs.readFileSync(filePath, 'utf8')
const { data: frontmatter, content } = matter(fileContent)
return (
<article>
<h1>{frontmatter.title}</h1>
<div className="meta">
<span>作者: {frontmatter.author}</span>
<span>日期: {frontmatter.date}</span>
<span>标签: {frontmatter.tags.join(', ')}</span>
</div>
<div className="content">
{content}
</div>
</article>
)
} catch (error) {
return <div>文章未找到</div>
}
}
export default BlogPost
2. 读取 JSON 配置文件
// app/settings/page.js
import fs from 'fs'
import path from 'path'
async function SettingsPage() {
const configPath = path.join(process.cwd(), 'config', 'settings.json')
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
return (
<div>
<h1>系统设置</h1>
<div className="settings">
<div>
<h3>站点信息</h3>
<p>站点名称: {config.site.name}</p>
<p>站点描述: {config.site.description}</p>
</div>
<div>
<h3>功能开关</h3>
<p>评论功能: {config.features.comments ? '开启' : '关闭'}</p>
<p>搜索功能: {config.features.search ? '开启' : '关闭'}</p>
</div>
</div>
</div>
)
}
export default SettingsPage
环境变量和配置
1. 使用环境变量
// app/weather/page.js
async function WeatherPage() {
const apiKey = process.env.WEATHER_API_KEY
const city = process.env.DEFAULT_CITY || 'Beijing'
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
)
if (!response.ok) {
return <div>天气数据获取失败</div>
}
const weather = await response.json()
return (
<div>
<h1>{city} 天气</h1>
<div className="weather">
<p>温度: {weather.main.temp}°C</p>
<p>湿度: {weather.main.humidity}%</p>
<p>天气: {weather.weather[0].description}</p>
</div>
</div>
)
}
export default WeatherPage
最佳实践
1. 数据获取函数分离
// lib/api.js
export async function getPosts() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 3600 },
})
return response.json()
}
export async function getPost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
next: { revalidate: 3600 },
})
return response.json()
}
// app/blog/page.js
import { getPosts } from '@/lib/api'
async function BlogPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
)
}
2. 错误边界处理
// app/blog/[id]/page.js
async function BlogPost({ params }) {
try {
const post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
} catch (error) {
return (
<div>
<h1>文章加载失败</h1>
<p>请稍后重试</p>
</div>
)
}
}
总结
Server Component 中的数据获取特点:
- 直接异步:使用 async/await 语法
- 服务器执行:在服务器端运行,不发送到客户端
- 缓存支持:内置缓存策略
- 错误处理:可以使用 try/catch
- 并行处理:支持 Promise.all 并行获取
- 类型安全:原生 TypeScript 支持
这些特性让 Server Component 成为数据获取的理想选择,提供了更好的性能和用户体验。