Skip to content

Next.js 数据获取

Next.js 提供了多种数据获取方法,支持不同的渲染策略和使用场景。本章将详细介绍各种数据获取方式及其最佳实践。

数据获取概述

Next.js 支持以下几种主要的数据获取方式:

  • getStaticProps - 静态生成时获取数据
  • getServerSideProps - 服务端渲染时获取数据
  • getStaticPaths - 动态路由的静态生成
  • 客户端数据获取 - 使用 useEffect 或 SWR/React Query
  • App Router 数据获取 - Next.js 13+ 的新方式

getStaticProps - 静态生成

getStaticProps 在构建时运行,适用于数据不经常变化的页面。

基本用法

javascript
// pages/blog.js
export default function Blog({ posts }) {
  return (
    <div>
      <h1>博客文章</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

export async function getStaticProps() {
  // 从 API 或文件系统获取数据
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // 可选:重新验证时间(秒)
    revalidate: 60, // ISR - 增量静态再生
  }
}

高级配置

javascript
export async function getStaticProps(context) {
  const { params, preview, previewData, locale } = context

  try {
    const posts = await fetchPosts()
    
    return {
      props: {
        posts,
      },
      // 重新验证设置
      revalidate: 3600, // 1小时
      // 404 处理
      notFound: posts.length === 0,
      // 重定向
      redirect: posts.length === 0 ? {
        destination: '/no-posts',
        permanent: false,
      } : undefined,
    }
  } catch (error) {
    return {
      notFound: true,
    }
  }
}

getServerSideProps - 服务端渲染

getServerSideProps 在每次请求时运行,适用于需要实时数据的页面。

基本用法

javascript
// pages/dashboard.js
export default function Dashboard({ user, stats }) {
  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <div>
        <p>今日访问量:{stats.todayVisits}</p>
        <p>总用户数:{stats.totalUsers}</p>
      </div>
    </div>
  )
}

export async function getServerSideProps(context) {
  const { req, res, query, params } = context
  
  // 获取用户信息(基于 cookie 或 session)
  const user = await getUserFromRequest(req)
  
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }

  // 获取实时统计数据
  const stats = await fetchUserStats(user.id)

  return {
    props: {
      user,
      stats,
    },
  }
}

错误处理和缓存

javascript
export async function getServerSideProps(context) {
  const { res } = context
  
  try {
    const data = await fetchData()
    
    // 设置缓存头
    res.setHeader(
      'Cache-Control',
      'public, s-maxage=10, stale-while-revalidate=59'
    )
    
    return {
      props: { data },
    }
  } catch (error) {
    console.error('数据获取失败:', error)
    
    return {
      props: {
        error: '数据加载失败',
      },
    }
  }
}

getStaticPaths - 动态路由

用于动态路由的静态生成,定义哪些路径需要预渲染。

基本用法

javascript
// pages/posts/[id].js
export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

export async function getStaticPaths() {
  // 获取所有文章 ID
  const posts = await fetchAllPosts()
  
  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }))

  return {
    paths,
    fallback: false, // 404 for non-existent paths
  }
}

export async function getStaticProps({ params }) {
  const post = await fetchPost(params.id)
  
  if (!post) {
    return {
      notFound: true,
    }
  }

  return {
    props: { post },
    revalidate: 3600,
  }
}

Fallback 策略

javascript
export async function getStaticPaths() {
  // 只预渲染最受欢迎的文章
  const popularPosts = await fetchPopularPosts()
  
  const paths = popularPosts.map(post => ({
    params: { id: post.id.toString() }
  }))

  return {
    paths,
    fallback: 'blocking', // 其他路径在首次请求时生成
  }
}

// 处理 fallback 状态
export default function Post({ post }) {
  const router = useRouter()
  
  if (router.isFallback) {
    return <div>加载中...</div>
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

客户端数据获取

使用 useEffect

javascript
import { useState, useEffect } from 'react'

export default function Profile() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch('/api/user')
        if (!response.ok) {
          throw new Error('获取用户信息失败')
        }
        const userData = await response.json()
        setUser(userData)
      } catch (err) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchUser()
  }, [])

  if (loading) return <div>加载中...</div>
  if (error) return <div>错误:{error}</div>
  if (!user) return <div>未找到用户</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

使用 SWR

SWR 是一个用于数据获取的 React Hook 库,提供缓存、重新验证等功能。

javascript
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then(res => res.json())

export default function Profile() {
  const { data: user, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return <div>加载失败</div>
  if (isLoading) return <div>加载中...</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

SWR 高级用法

javascript
import useSWR, { mutate } from 'swr'

export default function TodoList() {
  const { data: todos, error } = useSWR('/api/todos', fetcher, {
    refreshInterval: 1000, // 每秒刷新
    revalidateOnFocus: false, // 焦点时不重新验证
    dedupingInterval: 2000, // 2秒内的重复请求会被去重
  })

  const addTodo = async (text) => {
    // 乐观更新
    const newTodo = { id: Date.now(), text, completed: false }
    mutate('/api/todos', [...todos, newTodo], false)
    
    try {
      await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      })
      // 重新验证数据
      mutate('/api/todos')
    } catch (error) {
      // 回滚乐观更新
      mutate('/api/todos')
    }
  }

  return (
    <div>
      {todos?.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

App Router 数据获取 (Next.js 13+)

Next.js 13 引入了新的 App Router,提供了更简洁的数据获取方式。

服务端组件数据获取

javascript
// app/posts/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // 缓存1小时
  })
  
  if (!res.ok) {
    throw new Error('获取文章失败')
  }
  
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()
  
  return (
    <div>
      <h1>文章列表</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

并行数据获取

javascript
// app/dashboard/page.js
async function getUser() {
  const res = await fetch('https://api.example.com/user')
  return res.json()
}

async function getStats() {
  const res = await fetch('https://api.example.com/stats')
  return res.json()
}

export default async function Dashboard() {
  // 并行获取数据
  const [user, stats] = await Promise.all([
    getUser(),
    getStats()
  ])
  
  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <div>访问量:{stats.visits}</div>
    </div>
  )
}

流式渲染和 Suspense

javascript
// app/posts/page.js
import { Suspense } from 'react'

async function PostList() {
  const posts = await getPosts()
  
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

export default function PostsPage() {
  return (
    <div>
      <h1>文章列表</h1>
      <Suspense fallback={<div>加载文章中...</div>}>
        <PostList />
      </Suspense>
    </div>
  )
}

数据获取最佳实践

1. 选择合适的数据获取方法

javascript
// 静态内容 - 使用 getStaticProps
export async function getStaticProps() {
  const posts = await fetchBlogPosts()
  return { props: { posts }, revalidate: 3600 }
}

// 用户特定内容 - 使用 getServerSideProps
export async function getServerSideProps(context) {
  const user = await getUserFromSession(context.req)
  return { props: { user } }
}

// 实时更新内容 - 使用客户端获取
function LiveData() {
  const { data } = useSWR('/api/live-data', fetcher, {
    refreshInterval: 1000
  })
  return <div>{data?.value}</div>
}

2. 错误处理

javascript
// 统一错误处理
async function fetchWithErrorHandling(url) {
  try {
    const response = await fetch(url)
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return await response.json()
  } catch (error) {
    console.error('数据获取失败:', error)
    throw error
  }
}

// 在组件中使用
export async function getStaticProps() {
  try {
    const data = await fetchWithErrorHandling('/api/data')
    return { props: { data } }
  } catch (error) {
    return {
      props: { error: error.message },
      revalidate: 60, // 1分钟后重试
    }
  }
}

3. 性能优化

javascript
// 数据预取
import { useRouter } from 'next/router'
import Link from 'next/link'

function PostLink({ post }) {
  const router = useRouter()
  
  return (
    <Link
      href={`/posts/${post.id}`}
      onMouseEnter={() => {
        // 预取页面数据
        router.prefetch(`/posts/${post.id}`)
      }}
    >
      {post.title}
    </Link>
  )
}

// 条件数据获取
export async function getServerSideProps(context) {
  const { user } = context.req
  
  // 只为登录用户获取数据
  if (!user) {
    return {
      redirect: { destination: '/login', permanent: false }
    }
  }
  
  const userData = await fetchUserData(user.id)
  return { props: { userData } }
}

4. 缓存策略

javascript
// ISR 缓存策略
export async function getStaticProps() {
  const data = await fetchData()
  
  return {
    props: { data },
    revalidate: 60, // 60秒后重新生成
  }
}

// 客户端缓存
const { data } = useSWR('/api/data', fetcher, {
  dedupingInterval: 2000, // 2秒内去重
  focusThrottleInterval: 5000, // 5秒内限制焦点重新验证
  errorRetryInterval: 5000, // 错误重试间隔
})

总结

Next.js 提供了灵活的数据获取方案:

  • getStaticProps: 适用于静态内容,构建时获取数据
  • getServerSideProps: 适用于动态内容,每次请求时获取数据
  • getStaticPaths: 用于动态路由的静态生成
  • 客户端获取: 适用于用户交互和实时数据
  • App Router: Next.js 13+ 的新数据获取方式

选择合适的方法取决于你的具体需求:数据更新频率、SEO 要求、性能考虑等。合理使用这些方法可以构建高性能、用户体验良好的应用程序。

本站内容仅供学习和研究使用。