导航
导航
文章目录󰁋
  1. 一、服务端组件直接调用后端API
    1. 问题描述
    2. 错误示例
    3. 正确做法
  2. 二、路由处理程序的静态化问题
    1. 问题描述
    2. 错误示例
    3. 正确做法
  3. 三、客户端组件调用API路由
    1. 问题描述
    2. 错误示例
    3. 正确做法
  4. 四、Suspense组件的错误使用
    1. 问题描述
    2. 错误示例
    3. 正确做法
  5. 五、Context Providers的错误封装
    1. 问题描述
    2. 错误示例
    3. 正确做法
  6. 六、滥用”use client”指令
    1. 问题描述
    2. 正确理解
  7. 七、服务端组件与客户端组件的组合
    1. 问题描述
    2. 错误示例
    3. 正确做法
  8. 八、忽视数据重新验证
    1. 问题描述
    2. 错误示例
    3. 正确做法
  9. 九、try/catch中使用redirect
    1. 问题描述
    2. 错误示例
    3. 正确做法
  10. 十、忽视搜索引擎优化
    1. 问题描述
    2. 正确做法
  11. 总结
NEW
🚀

前端系统进阶指南

系统化学习前端知识

关注公众号

公众号:前端进价之旅

Next.js App Router开发避坑指南 常见错误与最佳实践

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() })
}

因为cookiesheaders、非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>
)
}
// app/page.js
import { useTheme } from './theme-provider'

function ThemeButton() {
const { theme } = useTheme()
return <button>主题: {theme}</button>
}

// 这样页面仍然是服务端组件
export default function Page() {
return <ThemeButton />
}

六、滥用”use client”指令

问题描述

很多开发者习惯在每个组件都加上”use client”,导致整个应用退化为纯客户端渲染,失去服务端渲染的优势。

正确理解

“use client”声明的是服务端组件和客户端组件的边界。当你在父组件中直接导入子组件时,子组件会自动继承父组件的渲染环境。

// 父组件是客户端组件
'use client'

import { useState } from 'react'
import Button from './button' // 不需要"use client"

export default function Parent() {
const [count, setCount] = useState(0)

return (
<div>
<span>{count}</span>
{/* Button会自动成为客户端组件的一部分 */}
<Button onClick={() => setCount(c => c + 1)} />
</div>
)
}

只有当组件使用了以下特性时才需要”use client”:useState/useEffect等Hook、事件处理函数、浏览器专用API、自定义Context Provider。

七、服务端组件与客户端组件的组合

问题描述

如何在客户端组件中使用服务端组件?直接导入是不行的。

错误示例

'use client'

import ServerComponent from './server-component'

export default function ClientComponent() {
return (
<div>
{/* 错误:服务端组件不能直接导入到客户端组件 */}
<ServerComponent />
</div>
)
}

正确做法

通过props传递服务端组件:

// app/page.js - 服务端组件
import ClientComponent from './client-component'
import ServerComponent from './server-component'

export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
// app/client-component.js - 客户端组件
'use client'

export default function ClientComponent({ children }) {
return (
<div>
<p>客户端组件</p>
{/* children就是传入的服务端组件 */}
{children}
</div>
)
}

这是因为props传递的是渲染结果,而非组件本身,服务端组件先在服务器上渲染完成,再将结果传递给客户端组件。

八、忽视数据重新验证

问题描述

使用Server Actions修改数据后,页面不会自动更新,需要手动触发重新验证。

错误示例

// app/actions.js
'use server'

export async function createPost(formData) {
const title = formData.get('title')

// 模拟数据库操作
await db.posts.create({ title })

// 错误:没有重新验证数据
return { success: true }
}
// app/page.js
import { createPost } from './actions'

export default function Page() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">提交</button>
</form>
)
}

提交数据后,页面不会自动更新,需要手动刷新。

正确做法

使用revalidatePath或revalidateTag重新验证数据:

// app/actions.js
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData) {
const title = formData.get('title')

await db.posts.create({ title })

// 正确:重新验证页面数据
revalidatePath('/posts')

return { success: true }
}

或者使用revalidateTag标记:

// app/actions.js
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost(formData) {
const title = formData.get('title')

await db.posts.create({ title })

// 重新验证特定标签的数据
revalidateTag('posts')

return { success: true }
}

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

九、try/catch中使用redirect

问题描述

redirect函数内部是通过抛出错误实现的,如果放在try/catch中会导致失效。

错误示例

'use server'

import { redirect } from 'next/navigation'

export async function login(formData) {
try {
const email = formData.get('email')

if (!email) {
// 错误:redirect在catch中无法生效
throw new Error('需要邮箱')
}

await doLogin(email)
redirect('/dashboard')
} catch (error) {
return { error: error.message }
}
}

正确做法

将redirect放在try/catch之外,或使用finally:

// 方法一:放在try之外
export async function login(formData) {
const email = formData.get('email')

if (!email) {
return { error: '需要邮箱' }
}

await doLogin(email)
redirect('/dashboard')
}

// 方法二:使用finally
export async function login(formData) {
try {
const email = formData.get('email')

if (!email) {
throw new Error('需要邮箱')
}

await doLogin(email)
} catch (error) {
return { error: error.message }
} finally {
// 正确:finally中redirect可以执行
redirect('/dashboard')
}
}

十、忽视搜索引擎优化

问题描述

Next.js App Router默认不自动生成metadata,需要手动配置。

正确做法

使用Next.js提供的Metadata API:

// app/page.js
import { Metadata } from 'next'

export const metadata: Metadata = {
title: '页面标题',
description: '页面描述',
openGraph: {
title: '社交分享标题',
description: '社交分享描述',
images: ['/og-image.jpg'],
},
}

export default function Page() {
return <h1>内容</h1>
}

对于动态路由:

// app/posts/[slug]/page.js
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)

return {
title: post.title,
description: post.excerpt,
}
}

总结

Next.js App Router带来了全新的开发范式,但也需要我们转变开发习惯:

  1. 服务端组件优先:尽量在服务端组件中完成数据获取和渲染
  2. 正确使用Suspense:将Suspense放在父组件,包裹异步子组件
  3. 合理划分客户端与服务端:只在必要时使用”use client”
  4. 注意数据重新验证:修改数据后使用revalidatePath或revalidateTag
  5. 避免常见陷阱:不要在try/catch中使用redirect,正确配置Context Provider

掌握这些最佳实践,能够帮助你更好地使用Next.js App Router,构建更高效、更优质的Web应用。

支持一下
扫一扫,支持poetries
  • 微信扫一扫
  • 支付宝扫一扫