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
注意几个关键点:
- 组件声明为
async function - 可以直接
await数据库查询 - 不能使用 React hooks(
useState、useEffect等) - 默认情况下,这个组件会在服务端渲染,客户端只收到 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秒钟做决定
先问自己三个问题:
- 这是 Server Component 吗? → 如果是,继续;如果不是(Client Component),跳到问题3
- 数据来自哪里?
- 自己的数据库 → 直接查数据库
- 第三方 API → 用 fetch
- 需要从 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>
)
}
关键决策解释
- 为什么直接查数据库? → Server Component,没必要绕 API 路由
- 为什么并行获取文章和作者? → 两个查询无依赖,并行更快
- 为什么推荐文章用 Suspense? → 推荐不重要,可以慢点加载,不阻塞主内容
- 为什么用 revalidate: 3600? → 文章内容不常变,1小时缓存足够,减轻数据库压力
结论
说了这么多,其实核心就几点:
- Server Component 里优先直接查数据库,除非你需要从 Client Component 获取或对接第三方 API。
- async/await 组件很简单,但记得用 Promise.all 并行获取、用 Suspense 优化加载体验。
- Next.js 15 默认不缓存了,根据数据特性选
force-cache、no-store或revalidate。 - 处理好错误,检查
response.ok,用error.tsx兜底,注意redirect别放 try/catch 里。
Server Components 的数据获取真的比客户端简单太多了。不用管 loading 状态、竞态问题、请求取消,写起来爽很多。如果你还在犹豫要不要迁移到 App Router,光这一点就值得试试。
下个项目不妨大胆用起来,遇到问题再回来翻翻这篇文章。
Next.js Server Components数据获取完整流程
从选择fetch vs数据库查询到async组件写法、缓存策略、错误处理的完整步骤
⏱️ 预计耗时: 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: 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: 配置缓存策略
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: 错误处理
检查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可以:
• 直接连接数据库(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组件怎么写?
```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的缓存策略怎么配置?
不缓存(实时数据):
```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的错误处理怎么做?
```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日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南

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