Next.js App Router作为Next.js 13引入的全新路由体系,带来了React服务端组件、Server Actions、流式渲染等强大特性。然而,新架构也带来了学习曲线和常见的使用误区。本文将基于实际开发经验,总结我们在使用Next.js App Router时经常遇到的10个问题及解决方案,帮助你避坑前行。
# 一、服务端组件直接调用后端API
# 问题描述
在传统的React开发模式中,我们习惯于在组件中调用后端API接口获取数据。但在Next.js App Router中,服务端组件可以直接在服务器上发起网络请求获取数据,完全不需要额外的API路由。
# 错误示例
// 错误:在服务端组件中调用自己的API路由
export default async function Page() {
// 这种方式需要创建多余的API路由,且API地址硬编码
const data = await fetch('http://localhost:3000/api/posts').then(res => res.json())
return (
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
# 正确做法
服务端组件可以直接调用外部API或数据库,无需创建中间API路由:
// 正确:直接在服务端组件中获取数据
export default async function Page() {
// 直接调用外部API,代码更简洁,性能更好
const data = await fetch('https://api.example.com/posts', {
// Next.js会自动缓存fetch请求
next: { revalidate: 3600 }
}).then(res => res.json())
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
这样做的优势包括:减少网络请求环节、避免API地址硬编码、代码更加简洁直接。
# 二、路由处理程序的静态化问题
# 问题描述
Next.js默认会将路由处理程序(Route Handlers)进行静态优化,这意味着你的动态数据可能被缓存,导致返回的一直是旧数据。
# 错误示例
// app/api/time/route.js
export async function GET() {
console.log('API被调用')
// 生产环境下会被静态缓存,时间永远不会变
return Response.json({ time: new Date().toLocaleTimeString() })
}
部署生产环境后,无论刷新多少次,时间都不会变化。这就是被静态处理了。
# 正确做法
使用动态函数强制开启动态渲染:
// 方法一:使用cookies或headers
export async function GET(request) {
const token = request.cookies.get('token')
return Response.json({ time: new Date().toLocaleTimeString() })
}
// 方法二:添加非GET方法
export async function GET() {
return Response.json({ time: new Date().toLocaleTimeString() })
}
export async function POST() {
return Response.json({ time: new Date().toLocaleTimeString() })
}
// 方法三:使用dynamic函数
export const dynamic = 'force-dynamic'
export async function GET() {
return Response.json({ time: new Date().toLocaleTimeString() })
}
因为cookies、headers、非GET请求等只有在实际请求时才能确定值,Next.js会自动将其转为动态处理。
# 三、客户端组件调用API路由
# 问题描述
有些开发者误以为在客户端组件中就不能直接调用外部API,其实客户端组件同样可以直接发起网络请求。
# 错误示例
'use client'
import { useState } from 'react'
export default function PostsPage() {
const [posts, setPosts] = useState([])
return (
<>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<button onClick={async () => {
// 错误:调用自己创建的API路由
const res = await fetch('/api/posts')
const data = await res.json()
setPosts(data)
}}>
获取文章
</button>
</>
)
}
# 正确做法
客户端组件可以直接调用外部API,无需中间层:
'use client'
import { useState } from 'react'
export default function PostsPage() {
const [posts, setPosts] = useState([])
return (
<>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<button onClick={async () => {
// 正确:直接调用外部API
const res = await fetch('https://api.example.com/posts')
const data = await res.json()
setPosts(data)
}}>
获取文章
</button>
</>
)
}
# 四、Suspense组件的错误使用
# 问题描述
Suspense用于流式渲染和加载状态展示,但很多开发者把它放错了位置,导致效果适得其反。
# 错误示例
import { Suspense } from 'react'
async function Posts() {
const data = await fetchPosts() // 模拟2秒延迟
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
return (
<div>
<h1>文章列表</h1>
{/* 错误:Suspense放在异步组件内部 */}
<Suspense fallback={<div>加载中...</div>}>
<Posts />
</Suspense>
</div>
)
}
这样写会导致整个页面都要等待Posts加载完成才能开始渲染。
# 正确做法
Suspense应该包裹异步组件,放在父组件层面:
import { Suspense } from 'react'
async function Posts() {
const data = await fetchPosts()
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default function Page() {
return (
<div>
<h1>文章列表</h1>
{/* 正确:Suspense在父组件中包裹异步子组件 */}
<Suspense fallback={<div>加载中...</div>}>
<Posts />
</Suspense>
</div>
)
}
这样页面骨架会立即渲染,加载状态显示在对应位置,实现真正的流式加载体验。
# 五、Context Providers的错误封装
# 问题描述
在App Router中,Context Providers必须放在客户端组件中。如果直接在整个页面使用Context,会导致整个页面变成客户端组件,失去服务端渲染的优势。
# 错误示例
'use client'
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext('light')
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button>主题: {theme}</button>
}
// 错误:整个页面变成客户端组件
export default function Page() {
return (
<ThemeContext.Provider value="dark">
<ThemeButton />
</ThemeContext.Provider>
)
}
这会导致页面无法享受服务端渲染的性能优势。
# 正确做法
将Provider组件独立出来,放在布局中:
// app/theme-provider.js
'use client'
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
return useContext(ThemeContext)
}
// app/layout.js
import { ThemeProvider } from './theme-provider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
)
}
# 六、滥用"use client"指令
# 问题描述
很多开发者习惯在每个组件都加上"use client",导致整个应用退化为纯客户端渲染,失去服务端渲染的优势。