Q7: 在 App Router 中,推荐的数据获取方法是什么?与 Pages Router 中的 getServerSideProps/getStaticPr

41 阅读2分钟

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 RouterApp 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 有以下优势:

  1. 更简单:直接在组件中获取数据,无需额外的函数
  2. 更灵活:组件级别的数据获取,支持更细粒度的控制
  3. 更强大:内置缓存策略,支持 ISR 和标签缓存
  4. 更类型安全:原生 TypeScript 支持
  5. 更高效:自动请求去重和并行处理

这些改进让数据获取变得更加直观和高效,同时保持了强大的缓存和性能优化能力。