Q8: 如何在 Server Component 中获取数据?请写出一个异步 Server Component 的例子。

43 阅读2分钟

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 中的数据获取特点:

  1. 直接异步:使用 async/await 语法
  2. 服务器执行:在服务器端运行,不发送到客户端
  3. 缓存支持:内置缓存策略
  4. 错误处理:可以使用 try/catch
  5. 并行处理:支持 Promise.all 并行获取
  6. 类型安全:原生 TypeScript 支持

这些特性让 Server Component 成为数据获取的理想选择,提供了更好的性能和用户体验。