切换语言
切换主题

Next.js Server Components 数据获取完全指南:fetch、数据库查询与最佳实践

第一次在 Next.js App Router 里写组件的时候,我盯着编辑器看了好久。

async function Page() {
  const data = await fetch('...')
  return <div>{data}</div>
}

就这?直接 await?不用 useEffect,不用 useState,也不用担心竞态问题?

说实话,那一刻我有点懵。习惯了 React 客户端那套模式,突然告诉你”组件可以是异步的”,感觉像是规则变了。更让人纠结的是:我到底该用 fetch API,还是直接查数据库?用 fetch 吧,感觉多了一层 API 调用;直接查数据库吧,又担心把数据库密钥暴露给客户端。

如果你也有这些困惑,这篇文章就是写给你的。咱们聊聊 Next.js Server Components 数据获取的正确姿势——什么时候用 fetch、什么时候查数据库、async 组件怎么写、缓存怎么控制,还有那些容易踩的坑。

Server Components 数据获取基础

为什么 Server Components 能直接 await?

先说答案:Server Components 跑在服务端,不是浏览器里。

听起来废话,但这个区别很关键。传统的 React 组件在浏览器中渲染,没法直接访问数据库或文件系统。但 Server Components 是在服务器上执行的,所以能做很多以前只能在 API 路由里做的事:

  • 直接连接数据库(Prisma、Drizzle、原生 SQL)
  • 读取文件系统(比如读 markdown 文件)
  • 调用内部服务(不用担心跨域)
  • 访问环境变量和密钥(不会暴露给客户端)

所以这段代码是完全安全的:

// app/posts/page.tsx
import { db } from '@/lib/db'

async function PostsPage() {
  // 直接查数据库,密钥不会发送到浏览器
  const posts = await db.post.findMany()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default PostsPage

注意几个关键点:

  1. 组件声明为 async function
  2. 可以直接 await 数据库查询
  3. 不能使用 React hooks(useStateuseEffect 等)
  4. 默认情况下,这个组件会在服务端渲染,客户端只收到 HTML

三种主要数据获取方式

在 Server Components 里,你有三个选择:

1. fetch API

最熟悉的方式,调用外部 API 或自己的 Route Handler:

async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  return <div>{data.title}</div>
}

2. 直接数据库查询

用 ORM 或数据库客户端直接查:

import { db } from '@/lib/db'

async function Page() {
  const data = await db.posts.findFirst()
  return <div>{data.title}</div>
}

3. Server Actions

用于数据变更(表单提交、删除等),不仅能获取数据,还能改数据:

async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title')
  await db.post.create({ data: { title } })
}

那到底该用哪个?往下看。

fetch vs 数据库查询 - 如何选择?

这是我刚开始用 App Router 时最纠结的问题。现在回头看,答案其实挺清楚的。

决策树:5秒钟做决定

先问自己三个问题:

  1. 这是 Server Component 吗? → 如果是,继续;如果不是(Client Component),跳到问题3
  2. 数据来自哪里?
    • 自己的数据库 → 直接查数据库
    • 第三方 API → 用 fetch
  3. 需要从 Client Component 获取数据吗? → 创建 API 路由,然后用 fetch

就这么简单。

为什么优先直接查数据库?

Next.js 官方的建议很明确:在 Server Component 里,别绕弯子去调 API 路由,直接查就行。

理由也很实在:

1. 省一次 HTTP 往返

看看区别:

// ❌ 绕远路:Server Component → API Route → Database
async function Page() {
  const res = await fetch('/api/posts')  // HTTP 调用
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ 直达:Server Component → Database
async function Page() {
  const posts = await db.post.findMany()  // 直接查询
  return <PostList posts={posts} />
}

第二种方式少了一层,响应更快。你可能觉得”能差多少”,但积少成多,页面加载快 100-200ms 是很明显的。

2. 更好的类型安全

如果你用 TypeScript + Prisma/Drizzle,直接查数据库能拿到完整的类型推导:

// 类型自动推导,编辑器提示完美
const post = await db.post.findFirst({
  include: { author: true, comments: true }
})

// post.author.name ← 有类型提示
// post.comments[0].content ← 也有

用 fetch 的话,你得手动定义类型或者用 as 断言,容易出错。

3. 代码更简洁

不用创建额外的 API 文件,不用处理 HTTP 状态码和错误,代码少了一半。

什么时候必须用 API/fetch?

也不是说完全不用 API 路由,有三种情况你还是得用:

情况1:Client Component 需要数据

客户端组件没法直接查数据库(毕竟跑在浏览器里),这时候需要 API 端点:

// app/api/posts/route.ts
export async function GET() {
  const posts = await db.post.findMany()
  return Response.json(posts)
}

// components/client-posts.tsx
'use client'
export function ClientPosts() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(setPosts)
  }, [])

  return <div>{/* 渲染 posts */}</div>
}

情况2:需要对外暴露 API

如果你的 Next.js 应用需要给其他服务(移动 App、第三方)提供数据,那就得创建公开的 API 端点。

情况3:对接第三方服务

调用 GitHub API、OpenAI API 之类的,没啥好说的,直接 fetch:

async function Page() {
  const res = await fetch('https://api.github.com/users/vercel')
  const user = await res.json()
  return <div>Followers: {user.followers}</div>
}

async/await 组件的正确写法

说完选什么,再说说怎么写。

基本模式:简单到不可思议

最基础的 async 组件就长这样:

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  })

  if (!product) {
    return <div>Product not found</div>
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </div>
  )
}

没有 loading 状态,没有 useEffect,就是等数据回来,然后渲染。干净。

并行 vs 串行:性能差距巨大

这里有个坑,我之前踩过。

看这两段代码,你觉得哪个更快?

// ❌ 串行:慢
async function Page() {
  const user = await db.user.findFirst()      // 等 200ms
  const posts = await db.post.findMany()      // 再等 150ms
  const comments = await db.comment.findMany()  // 又等 100ms
  // 总共 450ms
  return <Dashboard user={user} posts={posts} comments={comments} />
}

// ✅ 并行:快
async function Page() {
  const [user, posts, comments] = await Promise.all([
    db.user.findFirst(),       // 同时发起
    db.post.findMany(),        // 同时发起
    db.comment.findMany(),     // 同时发起
  ])
  // 总共 200ms(最慢的那个)
  return <Dashboard user={user} posts={posts} comments={comments} />
}

差距是 2 倍多!如果数据之间没有依赖关系,一定要用 Promise.all 并行获取。

当然,如果有依赖就另说了:

// 必须串行:后面的查询依赖前面的结果
async function Page({ params }) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  // 必须先拿到 user,才能查他的 posts
  const posts = await db.post.findMany({ where: { authorId: user.id } })
  return <Profile user={user} posts={posts} />
}

Suspense 边界:控制加载体验

你可能会想:“数据在服务端获取,用户看到的不就是白屏吗?”

对,但 Next.js 提供了 loading.js 和 Suspense 来改善这个问题。

方法1:loading.js 文件

在路由文件夹里创建 loading.tsx,自动生效:

// app/posts/loading.tsx
export default function Loading() {
  return <div>Loading posts...</div>
}

// app/posts/page.tsx
async function PostsPage() {
  const posts = await db.post.findMany()  // 慢查询
  return <PostList posts={posts} />
}

用户会先看到 “Loading posts…”,等数据回来再替换成实际内容。

方法2:手动 Suspense

想更精细地控制,可以手动包 Suspense:

import { Suspense } from 'react'

async function SlowComponent() {
  const data = await slowQuery()  // 3秒
  return <div>{data}</div>
}

async function FastComponent() {
  const data = await fastQuery()  // 0.5秒
  return <div>{data}</div>
}

export default function Page() {
  return (
    <div>
      <FastComponent />  {/* 快的先显示 */}
      <Suspense fallback={<div>Loading...</div>}>
        <SlowComponent />  {/* 慢的等着,不阻塞上面 */}
      </Suspense>
    </div>
  )
}

常见错误:Suspense 放错位置

这个我也犯过:

// ❌ 错误:Suspense 在 async 组件内部,不生效
async function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {await slowQuery()}  {/* Suspense 拦不住 */}
    </Suspense>
  )
}

// ✅ 正确:Suspense 在外面包住 async 组件
export default function Layout() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SlowPage />  {/* async 组件 */}
    </Suspense>
  )
}

Suspense 必须在 async 组件的外面,才能捕获到 promise。

请求自动去重:不用担心重复调用

还有个很酷的特性:同一个请求,在一次渲染中调用多次,Next.js 会自动去重。

async function Header() {
  const user = await db.user.findFirst()  // 查询1
  return <div>{user.name}</div>
}

async function Sidebar() {
  const user = await db.user.findFirst()  // 查询2,但不会真的执行
  return <div>{user.name}</div>
}

export default function Page() {
  return (
    <div>
      <Header />
      <Sidebar />
      {/* 实际只查询了一次数据库 */}
    </div>
  )
}

Next.js 会记住第一次的结果,后面的调用直接返回缓存。你可以放心地在多个组件里调用同一个数据源,不用担心性能。

缓存策略与数据重新验证

说到缓存,Next.js 15 有个大变化,很多人踩坑了。

Next.js 15 的缓存默认值变了

以前(Next.js 14):fetch 默认 cache: 'force-cache',会一直缓存。

现在(Next.js 15):fetch 默认 cache: 'no-store',不缓存,每次都重新获取。

为啥改?官方说是因为大家老被缓存坑,以为数据会实时更新,结果一直是旧的。现在默认不缓存,更符合直觉。

这意味着什么?如果你从 14 升到 15,可能发现页面变慢了——以前缓存的接口现在每次都在调。

三种缓存策略

根据数据特性选:

1. 完全缓存(静态站点适用)

async function BlogPost({ slug }) {
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'force-cache'  // 永久缓存,直到重新构建
  })
  return <article>{post.content}</article>
}

适合:博客文章、产品页、文档——内容很少变的。

2. 完全不缓存(实时数据)

async function StockPrice() {
  const price = await fetch('https://api.example.com/stock', {
    cache: 'no-store'  // 每次都重新获取
  })
  return <div>Current price: {price}</div>
}

适合:股票价格、实时评论、用户状态——必须是最新的。

3. 定时重新验证(ISR)

async function ProductList() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }  // 60秒后过期,触发重新获取
  })
  return <div>{products.map(p => <Card key={p.id} {...p} />)}</div>
}

适合:产品列表、新闻首页——允许几十秒的延迟,但不能太旧。

手动重新验证:数据变了立即更新

有时候你改了数据(比如用户发了帖子),需要立即刷新缓存。Next.js 提供两个 API:

1. revalidatePath(刷新整个页面)

'use server'
import { revalidatePath } from 'next/cache'

async function createPost(formData: FormData) {
  await db.post.create({ data: {...} })
  revalidatePath('/posts')  // 刷新 /posts 页面的缓存
}

2. revalidateTag(刷新特定标签)

更精细的控制:

// 获取数据时打标签
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }  // 打上 'posts' 标签
  })
  return res.json()
}

// 需要时刷新这个标签
'use server'
import { revalidateTag } from 'next/cache'

async function createPost() {
  await db.post.create({ data: {...} })
  revalidateTag('posts')  // 只刷新带 'posts' 标签的缓存
}

错误处理与性能优化

错误处理:别让页面崩溃

Server Components 获取数据失败,默认会炸掉整个页面。你得处理好错误。

方法1:try/catch

async function Page() {
  try {
    const data = await fetch('https://api.example.com/data')
    if (!data.ok) throw new Error('Failed to fetch')
    return <div>{data.title}</div>
  } catch (error) {
    return <div>Something went wrong. Please try again.</div>
  }
}

方法2:error.js 文件

在路由文件夹里创建 error.tsx,自动捕获该路由及其子路由的错误:

// app/posts/error.tsx
'use client'  // 错误边界必须是客户端组件

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

一个坑:redirect 在 try/catch 里会被拦截

// ❌ 错误:redirect 抛出的错误被 catch 住了
async function Page() {
  try {
    const user = await getUser()
    if (!user) redirect('/login')  // 这里抛出的错误被下面 catch 住
  } catch (error) {
    return <div>Error</div>  // redirect 失效!
  }
}

// ✅ 正确:redirect 放在 try/catch 外面
async function Page() {
  let user
  try {
    user = await getUser()
  } catch (error) {
    return <div>Error</div>
  }

  if (!user) redirect('/login')  // 这样才能正常跳转
}

常见错误和解决方案

我把我踩过的坑列一下:

错误1:服务端 fetch 用相对路径

// ❌ 错误:服务端没有 base URL
async function Page() {
  const data = await fetch('/api/posts')  // 报错!
}

// ✅ 正确:用绝对路径
async function Page() {
  const data = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/posts`)
}

// ✅ 更好:直接查数据库,不用 fetch
async function Page() {
  const posts = await db.post.findMany()
}

错误2:忘记检查 response.ok

// ❌ 错误:fetch 不会自动抛出错误
async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()  // 如果 404,这里会出问题
  return <div>{data.title}</div>
}

// ✅ 正确:检查状态
async function Page() {
  const res = await fetch('https://api.example.com/data')

  if (!res.ok) {
    throw new Error(`HTTP error! status: ${res.status}`)
  }

  const data = await res.json()
  return <div>{data.title}</div>
}

错误3:在 Server Component 里调用 Route Handler

// ❌ 不推荐:多此一举
async function Page() {
  const res = await fetch('/api/posts')  // 为啥要绕这一圈?
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ 推荐:直接查
async function Page() {
  const posts = await db.post.findMany()
  return <PostList posts={posts} />
}

实战案例:构建一个博客页面

说了这么多理论,来个完整例子。

假设我们要做一个博客文章详情页,需要:

  • 显示文章内容
  • 显示作者信息
  • 显示相关文章推荐

文件结构

app/
  posts/
    [slug]/
      page.tsx       ← 文章详情页
      loading.tsx    ← 加载状态
      error.tsx      ← 错误处理

实现代码

// app/posts/[slug]/page.tsx
import { db } from '@/lib/prisma'
import { Suspense } from 'react'
import { notFound } from 'next/navigation'

// 主页面组件
export default async function PostPage({
  params,
}: {
  params: { slug: string }
}) {
  // 并行获取文章和作者信息
  const [post, author] = await Promise.all([
    db.post.findUnique({
      where: { slug: params.slug },
    }),
    db.user.findFirst(),
  ])

  if (!post) {
    notFound()  // 显示 404 页面
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <AuthorCard author={author} />
      <div>{post.content}</div>

      {/* 相关文章推荐可以慢点加载,不阻塞主内容 */}
      <Suspense fallback={<div>Loading recommendations...</div>}>
        <RecommendedPosts currentPostId={post.id} />
      </Suspense>
    </article>
  )
}

// 作者卡片(直接渲染,数据已经有了)
function AuthorCard({ author }) {
  return (
    <div>
      <img src={author.avatar} alt={author.name} />
      <span>{author.name}</span>
    </div>
  )
}

// 推荐文章(异步组件,独立加载)
async function RecommendedPosts({ currentPostId }: { currentPostId: string }) {
  const recommended = await db.post.findMany({
    where: {
      id: { not: currentPostId },
      published: true,
    },
    take: 3,
  })

  return (
    <div>
      <h3>You might also like</h3>
      {recommended.map((post) => (
        <a key={post.id} href={`/posts/${post.slug}`}>
          {post.title}
        </a>
      ))}
    </div>
  )
}

// 缓存策略:文章内容1小时重新验证一次
export const revalidate = 3600

// 生成静态参数(可选,用于静态生成)
export async function generateStaticParams() {
  const posts = await db.post.findMany({
    select: { slug: true },
  })

  return posts.map((post) => ({
    slug: post.slug,
  }))
}
// app/posts/[slug]/loading.tsx
export default function Loading() {
  return (
    <div>
      <div className="skeleton h-12 w-3/4" />
      <div className="skeleton h-4 w-1/4 mt-4" />
      <div className="skeleton h-64 mt-8" />
    </div>
  )
}
// app/posts/[slug]/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Failed to load post</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

关键决策解释

  1. 为什么直接查数据库? → Server Component,没必要绕 API 路由
  2. 为什么并行获取文章和作者? → 两个查询无依赖,并行更快
  3. 为什么推荐文章用 Suspense? → 推荐不重要,可以慢点加载,不阻塞主内容
  4. 为什么用 revalidate: 3600? → 文章内容不常变,1小时缓存足够,减轻数据库压力

结论

说了这么多,其实核心就几点:

  1. Server Component 里优先直接查数据库,除非你需要从 Client Component 获取或对接第三方 API。
  2. async/await 组件很简单,但记得用 Promise.all 并行获取、用 Suspense 优化加载体验。
  3. Next.js 15 默认不缓存了,根据数据特性选 force-cacheno-storerevalidate
  4. 处理好错误,检查 response.ok,用 error.tsx 兜底,注意 redirect 别放 try/catch 里。

Server Components 的数据获取真的比客户端简单太多了。不用管 loading 状态、竞态问题、请求取消,写起来爽很多。如果你还在犹豫要不要迁移到 App Router,光这一点就值得试试。

下个项目不妨大胆用起来,遇到问题再回来翻翻这篇文章。

Next.js Server Components数据获取完整流程

从选择fetch vs数据库查询到async组件写法、缓存策略、错误处理的完整步骤

⏱️ 预计耗时: 1 小时

  1. 1

    步骤1: 选择fetch vs 数据库查询

    直接查数据库(推荐):
    • 更快:少一层API调用,延迟更低
    • 更安全:数据库密钥不会暴露给客户端
    • 适用:数据库在服务端可访问

    代码示例:
    ```tsx
    import { db } from '@/lib/db'

    export default async function Page() {
    const users = await db.user.findMany()
    return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>
    }
    ```

    使用fetch:
    • 适用:对接第三方API
    • 适用:需要跨域的场景
    • 注意:Next.js 15默认不缓存

    代码示例:
    ```tsx
    export default async function Page() {
    const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // Next.js 15需要显式配置
    })
    const data = await res.json()
    return <div>{data}</div>
    }
    ```

    选择建议:优先直接查数据库,只有需要对接第三方API时才用fetch
  2. 2

    步骤2: async/await组件写法

    直接标记为async:
    ```tsx
    export default async function Page() {
    const data = await fetchData()
    return <div>{data}</div>
    }
    ```

    并行获取(Promise.all):
    ```tsx
    export default async function Page() {
    const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
    ])
    return <div>...</div>
    }
    ```

    用Suspense优化加载:
    ```tsx
    import { Suspense } from 'react'

    export default function Page() {
    return (
    <Suspense fallback={<div>Loading...</div>}>
    <UserList />
    </Suspense>
    )
    }

    async function UserList() {
    const users = await fetchUsers()
    return <div>{users.map(...)}</div>
    }
    ```

    关键点:
    • Server Components可以直接async
    • 用Promise.all并行获取
    • 用Suspense优化加载体验
  3. 3

    步骤3: 配置缓存策略

    Next.js 15默认不缓存,需要显式配置:

    不缓存(实时数据):
    ```tsx
    fetch(url, { cache: 'no-store' })
    ```

    永久缓存(静态数据):
    ```tsx
    fetch(url, { cache: 'force-cache' })
    ```

    定时更新(ISR):
    ```tsx
    fetch(url, { next: { revalidate: 3600 } })
    ```

    选择建议:
    • 实时数据 → cache: 'no-store'
    • 静态数据 → cache: 'force-cache'
    • 频繁更新但不需要实时 → revalidate

    注意:Next.js 15的哲学是"显式优于隐式",需要主动思考哪些数据需要缓存。
  4. 4

    步骤4: 错误处理

    检查response.ok:
    ```tsx
    const res = await fetch(url)
    if (!res.ok) {
    throw new Error('Failed to fetch')
    }
    const data = await res.json()
    ```

    使用error.tsx兜底:
    ```tsx
    // app/page/error.tsx
    'use client'
    export default function Error({ error, reset }) {
    return (
    <div>
    <h2>出错了: {error.message}</h2>
    <button onClick={reset}>重试</button>
    </div>
    )
    }
    ```

    注意redirect:
    ```tsx
    // ❌ 错误:redirect在try/catch里
    try {
    if (!user) redirect('/login')
    } catch (e) {
    // redirect会抛出错误,被catch捕获
    }

    // ✅ 正确:redirect在try/catch外
    if (!user) redirect('/login')
    try {
    // 其他逻辑
    } catch (e) {
    // 错误处理
    }
    ```

    关键点:
    • 检查response.ok
    • 用error.tsx兜底
    • redirect别放try/catch里

常见问题

Server Components为什么能直接await?
原因:Server Components跑在服务端,不是浏览器里。

Server Components可以:
• 直接连接数据库(Prisma、Drizzle、原生SQL)
• 读取文件系统(比如读markdown文件)
• 调用内部服务(不用担心跨域)
• 访问环境变量和密钥(不会暴露给客户端)

所以这段代码是完全安全的:
```tsx
import { db } from '@/lib/db'

export default async function Page() {
const users = await db.user.findMany() // 数据库密钥不会暴露
return <div>{users.map(...)}</div>
}
```

优势:
• 不用useEffect、useState
• 不用担心竞态问题
• 不用处理loading状态
• 数据获取比客户端简单太多
什么时候用fetch,什么时候直接查数据库?
直接查数据库(推荐):
• 更快:少一层API调用,延迟更低(服务端到数据库通常<10ms,客户端到服务端可能100ms+)
• 更安全:数据库密钥不会暴露给客户端
• 适用:数据库在服务端可访问

使用fetch:
• 适用:对接第三方API
• 适用:需要跨域的场景
• 适用:API已经存在,不想改架构

选择建议:
• 优先直接查数据库
• 只有需要对接第三方API时才用fetch
• 避免在Server Component里fetch自己的API(多此一举)

代码对比:
```tsx
// ❌ 反模式:在Server Component里fetch自己的API
const res = await fetch('/api/users')
const users = await res.json()

// ✅ 正确:直接查数据库
const users = await db.user.findMany()
```
async组件怎么写?
直接标记为async:
```tsx
export default async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
```

并行获取(Promise.all):
```tsx
export default async function Page() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
return <div>...</div>
}
```

用Suspense优化加载:
```tsx
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserList />
</Suspense>
)
}

async function UserList() {
const users = await fetchUsers()
return <div>{users.map(...)}</div>
}
```

关键点:
• Server Components可以直接async
• 用Promise.all并行获取
• 用Suspense优化加载体验
Next.js 15的缓存策略怎么配置?
Next.js 15默认不缓存,需要显式配置:

不缓存(实时数据):
```tsx
fetch(url, { cache: 'no-store' })
```

永久缓存(静态数据):
```tsx
fetch(url, { cache: 'force-cache' })
```

定时更新(ISR):
```tsx
fetch(url, { next: { revalidate: 3600 } })
```

选择建议:
• 实时数据 → cache: 'no-store'
• 静态数据 → cache: 'force-cache'
• 频繁更新但不需要实时 → revalidate

注意:Next.js 15的哲学是"显式优于隐式",需要主动思考哪些数据需要缓存。这是破坏性变更,迁移时需要注意。
Server Components的错误处理怎么做?
检查response.ok:
```tsx
const res = await fetch(url)
if (!res.ok) {
throw new Error('Failed to fetch')
}
const data = await res.json()
```

使用error.tsx兜底:
```tsx
// app/page/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>出错了: {error.message}</h2>
<button onClick={reset}>重试</button>
</div>
)
}
```

注意redirect:
```tsx
// ❌ 错误:redirect在try/catch里
try {
if (!user) redirect('/login')
} catch (e) {
// redirect会抛出错误,被catch捕获
}

// ✅ 正确:redirect在try/catch外
if (!user) redirect('/login')
try {
// 其他逻辑
} catch (e) {
// 错误处理
}
```

关键点:
• 检查response.ok
• 用error.tsx兜底
• redirect别放try/catch里(redirect会抛出错误)

11 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章