name: nextjs-15-patterns
description: Next.js 15 App Router 模式与最佳实践。
Next.js 15 模式
服务器组件 vs 客户端组件
默认:服务器组件
// app/users/page.tsx - 服务器组件(默认)
export default async function UsersPage() {
const users = await getUsers(); // 直接访问数据库
return <UserList users={users} />;
}
客户端组件(需要时使用)
// components/counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
服务器操作
// actions/user-actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export async function createUser(formData: FormData) {
const validated = CreateUserSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
});
if (!validated.success) {
return { error: validated.error.flatten() };
}
const user = await db.insert(users).values(validated.data).returning();
revalidatePath('/users');
return { data: user };
}
数据获取
在服务器组件中
// 直接使用 async/await - 无需 useEffect
async function UserProfile({ id }: { id: string }) {
const user = await getUser(id);
if (!user) notFound();
return <Profile user={user} />;
}
加载状态处理
// app/users/loading.tsx
export default function Loading() {
return <UserListSkeleton />;
}
错误处理
// app/users/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>出错了</h2>
<button onClick={reset}>重试</button>
</div>
);
}
路由处理器(API)
// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const users = await getUsers();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}
元数据
// app/users/[id]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { id: string };
}): Promise<Metadata> {
const user = await getUser(params.id);
return {
title: user?.name ?? '用户',
description: `${user?.name} 的个人资料`,
};
}
并行路由
app/
├── @modal/
│ └── (.)photo/[id]/page.tsx # 拦截式模态框
├── layout.tsx
└── page.tsx
最佳实践
- 优先使用服务器组件 - 仅在需要时使用 ‘use client’
- 数据获取就近原则 - 在数据使用的地方获取数据
- 使用服务器操作 - 用于数据变更,而非 API 路由
- 结合 Suspense 实现流式渲染 - 实现渐进式加载
- 验证所有输入 - 在服务器操作中使用 Zod 进行验证