名称: nextjs-fullstack-patterns
描述: 使用App Router模式、服务器组件、数据获取策略、认证和部署构建生产级Next.js应用程序。涵盖从数据库到UI的完整技术栈。触发于Next.js开发、React服务器组件、App Router或全栈TypeScript请求。
许可证: MIT
Next.js 全栈模式
现代Next.js应用程序的生产模式。
App Router 基础
基于文件的路由
app/
├── layout.tsx # 根布局(必需)
├── page.tsx # 首页(/)
├── loading.tsx # 加载UI
├── error.tsx # 错误边界
├── not-found.tsx # 404页面
├── globals.css # 全局样式
│
├── (marketing)/ # 路由组(不影响URL)
│ ├── about/
│ │ └── page.tsx # /about
│ └── blog/
│ └── page.tsx # /blog
│
├── dashboard/
│ ├── layout.tsx # 仪表板布局
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /dashboard/settings
│
├── api/
│ └── route.ts # API路由(/api)
│
└── [slug]/
└── page.tsx # 动态路由
路由约定
| 文件 |
用途 |
page.tsx |
路由的唯一UI |
layout.tsx |
共享UI,保留状态 |
template.tsx |
类似布局但重新挂载 |
loading.tsx |
加载UI(Suspense) |
error.tsx |
错误边界 |
not-found.tsx |
404 UI |
route.ts |
API端点 |
服务器组件 vs 客户端组件
默认:服务器组件
// app/posts/page.tsx(默认服务器组件)
async function PostsPage() {
const posts = await db.posts.findMany(); // 直接数据库访问
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default PostsPage;
客户端组件
'use client'; // 必需指令
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
计数:{count}
</button>
);
}
何时使用每种组件
| 服务器组件 |
客户端组件 |
| 数据获取 |
事件处理程序(onClick等) |
| 访问后端资源 |
useState, useEffect |
| 敏感数据(令牌、密钥) |
浏览器API |
| 大型依赖项 |
交互性 |
| SEO关键内容 |
实时更新 |
组合模式
// 服务器组件带客户端岛
import { Counter } from './counter'; // 客户端组件
async function Page() {
const data = await fetchData(); // 服务器端
return (
<div>
<h1>{data.title}</h1>
<Counter /> {/* 客户端岛 */}
</div>
);
}
数据获取
服务器组件数据获取
// 组件中直接使用async/await
async function Page() {
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache', // 默认:缓存
// cache: 'no-store', // 无缓存(动态)
// next: { revalidate: 60 } // ISR:每60秒重新验证
});
return <div>{data.title}</div>;
}
并行数据获取
async function Page() {
// 并行获取(不要顺序等待)
const [posts, comments] = await Promise.all([
getPosts(),
getComments(),
]);
return (
<>
<Posts data={posts} />
<Comments data={comments} />
</>
);
}
使用Suspense的流式传输
import { Suspense } from 'react';
async function Page() {
return (
<div>
<h1>仪表板</h1>
{/* 准备好时流式传输 */}
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
</div>
);
}
服务器操作
表单操作
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.posts.create({ data: { title } });
revalidatePath('/posts');
redirect('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">创建</button>
</form>
);
}
使用useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '保存中...' : '保存'}
</button>
);
}
使用useActionState
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
export function PostForm() {
const [state, action, pending] = useActionState(createPost, null);
return (
<form action={action}>
<input name="title" />
{state?.error && <p>{state.error}</p>}
<button disabled={pending}>创建</button>
</form>
);
}
API路由
路由处理器
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await db.posts.findMany();
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await db.posts.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
动态路由处理器
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id }
});
if (!post) {
return NextResponse.json(
{ error: '未找到' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
认证模式
中间件认证
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token'); <!-- allow-secret -->
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};
认证上下文模式
// lib/auth.ts
import { cookies } from 'next/headers';
import { cache } from 'react';
export const getUser = cache(async () => {
const token = cookies().get('token')?.value; <!-- allow-secret -->
if (!token) return null;
// 验证令牌,获取用户
return await validateAndFetchUser(token);
});
// app/dashboard/page.tsx
import { getUser } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const user = await getUser();
if (!user) {
redirect('/login');
}
return <div>欢迎,{user.name}</div>;
}
数据库模式
Prisma 设置
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}
数据访问层
// lib/data/posts.ts
import { db } from '@/lib/db';
import { cache } from 'react';
export const getPosts = cache(async () => {
return db.post.findMany({
orderBy: { createdAt: 'desc' },
include: { author: true },
});
});
export const getPost = cache(async (id: string) => {
return db.post.findUnique({
where: { id },
include: { author: true },
});
});
错误处理
错误边界
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了!</h2>
<button onClick={() => reset()}>重试</button>
</div>
);
}
未找到
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation';
export default async function Post({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
if (!post) {
notFound();
}
return <article>{post.content}</article>;
}
元数据与SEO
静态元数据
// app/about/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '关于我们',
description: '了解我们的公司',
};
动态元数据
// app/posts/[id]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({
params
}: {
params: { id: string }
}): Promise<Metadata> {
const post = await getPost(params.id);
return {
title: post?.title,
description: post?.excerpt,
};
}
项目结构
├── app/ # App Router
├── components/
│ ├── ui/ # 可重用UI组件
│ └── features/ # 特定功能组件
├── lib/
│ ├── db.ts # 数据库客户端
│ ├── auth.ts # 认证工具
│ └── utils.ts # 通用工具
├── actions/ # 服务器操作
├── types/ # TypeScript类型
├── hooks/ # 自定义React钩子
└── public/ # 静态资源
参考
references/deployment-checklist.md - 生产部署
references/performance-patterns.md - 优化技术
references/testing-patterns.md - 测试Next.js应用