Next.js 13服务器组件中从API路由获取数据的疑问
Next.js 13:服务器组件中通过API路由获取PocketBase数据的最佳实践
我正在学习Next.js 13,尝试从PocketBase数据库获取数据并展示在表格中。已经创建了连接数据库并获取数据的API路由,但在服务器组件中获取数据时存在困惑。
项目结构
app/ api/ ingresos/ route.ts components/ tablaIngresosCliente.tsx ingresos/ page.tsx ...
API路由代码(app/api/ingresos/route.ts)
import { NextResponse } from 'next/server'; import PocketBase from 'pocketbase'; export async function GET(request: Request) { const pb = new PocketBase('http://127.0.0.1:8090'); try { const records = await pb.collection('ingresos').getFullList({ sort: '-created', }); return NextResponse.json(records); } catch (error) { console.error(error) return new Response('Could not fetch', { status: 500 }) } }
服务器组件代码(app/components/tablaIngresosCliente.tsx)
export default async function TablaIngresos() { const res = await fetch('/api/ingresos'); const ingresos = await res.json(); return ( <div className="flex flex-col"> <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="inline-block min-w-full py-2 sm:px-6 lg:px-8"> <div className="overflow-hidden"> <table className="min-w-full text-center text-sm font-light"> <thead className="border-b bg-neutral-800 font-medium text-white dark:border-neutral-500 dark:bg-neutral-900"> <tr> <th scope="col" className=" px-6 py-4">#</th> <th scope="col" className=" px-6 py-4">Fuente</th> <th scope="col" className=" px-6 py-4">Cantidad</th> <th scope="col" className=" px-6 py-4">Fecha</th> </tr> </thead> <tbody> {ingresos.map((item, index) => ( <tr key={index} className="border-b dark:border-neutral-500"> <td className="whitespace-nowrap px-6 py-4 font-medium">{index + 1}</td> <td className="whitespace-nowrap px-6 py-4">{item.fuente}</td> <td className="whitespace-nowrap px-6 py-4">{item.cantidad}</td> <td className="whitespace-nowrap px-6 py-4">{item.fecha}</td> </tr> ))} </tbody> </table> </div> </div> </div> </div> ); }
当前访问/api/ingresos路由能得到正确响应,但不确定在服务器组件中这样调用API路由是否合理。了解可以使用服务器组件Actions,但选择API路由是因为计划用客户端组件发送POST请求添加数据,认为API路由可同时处理GET和POST请求。想知道:
- 这是否是Next.js 13中获取数据的正确方式?
- 是否推荐在服务器组件中从API路由获取数据?
- 有没有更好的数据获取与展示方式?
回答
1. 当前方式是否正确?
你的代码本身可以正常运行:服务器组件中使用fetch调用内部API路由时,Next.js会在服务器端直接执行该请求(不会发起外部网络请求),最终能正确渲染数据表格。但这不是最优化的实现方式。
2. 是否推荐服务器组件调用内部API路由?
不推荐,核心原因如下:
- 冗余层级:服务器组件本身运行在服务器端,直接调用PocketBase SDK获取数据,比通过API路由中转少一层处理,性能更优。
- 重复逻辑:API路由和服务器组件都需要初始化PocketBase实例,导致代码重复,维护成本增加。
- 调试复杂度:多一层API路由意味着排查问题时需要多检查一个环节,增加调试难度。
3. 更好的实现方案
结合你需要同时支持客户端POST请求的需求,推荐以下两种方案:
方案一:共享数据获取逻辑
将PocketBase的初始化和数据获取逻辑抽离为独立工具函数,在API路由和服务器组件中复用:
- 创建工具文件
app/lib/pocketbase.ts:
import PocketBase from 'pocketbase'; export function getPocketBase() { return new PocketBase('http://127.0.0.1:8090'); } export async function getIngresos() { const pb = getPocketBase(); try { return await pb.collection('ingresos').getFullList({ sort: '-created', }); } catch (error) { console.error(error); throw new Error('Could not fetch ingresos'); } }
- 修改API路由
app/api/ingresos/route.ts:
import { NextResponse } from 'next/server'; import { getIngresos, getPocketBase } from '@/app/lib/pocketbase'; export async function GET(request: Request) { try { const records = await getIngresos(); return NextResponse.json(records); } catch (error) { return new Response('Could not fetch', { status: 500 }); } } // 新增POST请求处理逻辑 export async function POST(request: Request) { const pb = getPocketBase(); try { const data = await request.json(); const record = await pb.collection('ingresos').create(data); return NextResponse.json(record, { status: 201 }); } catch (error) { return new Response('Could not create record', { status: 500 }); } }
- 修改服务器组件
app/components/tablaIngresosCliente.tsx:
import { getIngresos } from '@/app/lib/pocketbase'; export default async function TablaIngresos() { const ingresos = await getIngresos(); return ( <div className="flex flex-col"> <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="inline-block min-w-full py-2 sm:px-6 lg:px-8"> <div className="overflow-hidden"> <table className="min-w-full text-center text-sm font-light"> <thead className="border-b bg-neutral-800 font-medium text-white dark:border-neutral-500 dark:bg-neutral-900"> <tr> <th scope="col" className=" px-6 py-4">#</th> <th scope="col" className=" px-6 py-4">Fuente</th> <th scope="col" className=" px-6 py-4">Cantidad</th> <th scope="col" className=" px-6 py-4">Fecha</th> </tr> </thead> <tbody> {ingresos.map((item) => ( <tr key={item.id} className="border-b dark:border-neutral-500"> <td className="whitespace-nowrap px-6 py-4 font-medium">{item.id}</td> <td className="whitespace-nowrap px-6 py-4">{item.fuente}</td> <td className="whitespace-nowrap px-6 py-4">{item.cantidad}</td> <td className="whitespace-nowrap px-6 py-4">{new Date(item.fecha).toLocaleDateString()}</td> </tr> ))} </tbody> </table> </div> </div> </div> </div> ); }
优化点说明:
- 用
item.id代替index作为React元素的key,符合React最佳实践,避免列表更新时出现异常。 - 对日期字段做格式化处理,提升前端展示的可读性。
- 共享逻辑避免代码重复,提升代码维护性。
方案二:使用Server Actions处理POST请求
如果不需要对外暴露API路由,可以直接使用Server Actions处理客户端的POST请求,同时在服务器组件中直接获取数据:
- 服务器组件代码同方案一的组件实现。
- 创建客户端组件处理表单提交:
'use client'; import { getPocketBase } from '@/app/lib/pocketbase'; import { useFormState } from 'react-dom'; async function addIngreso(formData: FormData) { 'use server'; const pb = getPocketBase(); try { await pb.collection('ingresos').create({ fuente: formData.get('fuente'), cantidad: formData.get('cantidad'), fecha: formData.get('fecha'), }); return { success: true }; } catch (error) { return { success: false, error: 'Failed to add ingreso' }; } } export default function IngresoForm() { const [state, formAction] = useFormState(addIngreso, { success: false }); return ( <form action={formAction} className="space-y-4"> <div> <label>Fuente:</label> <input type="text" name="fuente" required className="ml-2 p-1 border" /> </div> <div> <label>Cantidad:</label> <input type="number" name="cantidad" required className="ml-2 p-1 border" /> </div> <div> <label>Fecha:</label> <input type="date" name="fecha" required className="ml-2 p-1 border" /> </div> <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">Agregar Ingreso</button> {state.error && <p className="text-red-500 mt-2">{state.error}</p>} {state.success && <p className="text-green-500 mt-2">Ingreso agregado exitosamente!</p>} </form> ); }
优势:
- 无需维护额外的API路由,代码结构更简洁。
- Server Actions内置CSRF保护,安全性更高。
- 表单状态处理更便捷,无需手动编写fetch请求逻辑。
总结
- 你的当前实现可以运行,但存在冗余,不推荐服务器组件调用内部API路由。
- 优先选择共享数据获取逻辑的方案,兼顾API路由的对外能力和服务器组件的性能。
- 如果不需要对外暴露API,Server Actions是更轻量、安全的选择。
内容的提问来源于stack exchange,提问作者Vladimirzb
相关产品推荐
相关产品推荐

