nextjs-15
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen to Use
适用场景
Triggers: When building Next.js apps, working with app router, server/client components, or API routes.
Load when: building Next.js 15 apps, using app router, implementing server actions, fetching data, or setting up middleware.
触发场景:构建Next.js应用、使用App Router、开发服务端/客户端组件或API路由时。
适用时机:构建Next.js 15应用、使用App Router、实现Server Actions、获取数据或配置中间件时。
Critical Patterns
核心模式
Pattern 1: Server Components by default
模式1:默认使用Server Components
typescript
// ✅ Server Component — async by default, no directive needed
async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findById(userId); // Direct DB access
return <ProfileCard user={user} />;
}
// ✅ Client Component — only when you need interactivity
'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}typescript
// ✅ Server Component — 默认支持异步,无需指令
async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findById(userId); // 直接访问数据库
return <ProfileCard user={user} />;
}
// ✅ Client Component — 仅在需要交互时使用
'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}Pattern 2: Server Actions
模式2:Server Actions
typescript
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
await db.users.create({ name, email });
revalidatePath('/users');
redirect('/users');
}
// Direct usage in form
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Create</button>
</form>
);
}typescript
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
await db.users.create({ name, email });
revalidatePath('/users');
redirect('/users');
}
// 在表单中直接使用
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Create</button>
</form>
);
}Pattern 3: Prevent client-side access with server-only
模式3:使用server-only阻止客户端访问
typescript
// lib/db.ts
import 'server-only'; // Build error if imported in client
export async function getSecretData() {
return db.secrets.findAll();
}typescript
// lib/db.ts
import 'server-only'; // 若在客户端导入会触发构建错误
export async function getSecretData() {
return db.secrets.findAll();
}Code Examples
代码示例
Data Fetching — Parallel and Streaming
数据获取 — 并行与流式传输
typescript
// ✅ Parallel fetching in Server Component
async function Dashboard() {
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats(),
]);
return <DashboardView user={user} posts={posts} stats={stats} />;
}
// ✅ Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header /> {/* Immediate */}
<Suspense fallback={<PostsSkeleton />}>
<Posts /> {/* Streams when ready */}
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Waits here
return <PostList posts={posts} />;
}typescript
// ✅ Server Component中的并行获取
async function Dashboard() {
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats(),
]);
return <DashboardView user={user} posts={posts} stats={stats} />;
}
// ✅ 结合Suspense实现流式传输
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header /> {/* 立即渲染 */}
<Suspense fallback={<PostsSkeleton />}>
<Posts /> {/* 准备就绪后流式渲染 */}
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // 在此处等待数据
return <PostList posts={posts} />;
}API Route Handler
API路由处理器
typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') ?? '1';
const users = await db.users.findMany({ page: parseInt(page) });
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') ?? '1';
const users = await db.users.findMany({ page: parseInt(page) });
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}Middleware — Route Protection
中间件 — 路由保护
typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};Metadata — Static and Dynamic
元数据 — 静态与动态
typescript
// Static
export const metadata = {
title: 'My App',
description: 'App description',
};
// Dynamic
export async function generateMetadata({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}typescript
// 静态元数据
export const metadata = {
title: 'My App',
description: 'App description',
};
// 动态元数据
export async function generateMetadata({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}Route Groups and Layouts
路由组与布局
app/
├── (auth)/ # Group with no URL impact
│ ├── layout.tsx # Layout only for auth pages
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── overview/page.tsx # /overview
├── _components/ # Private folder (not a route)
├── layout.tsx # Root layout (required)
└── page.tsx # /app/
├── (auth)/ # 无URL影响的路由组
│ ├── layout.tsx # 仅适用于认证页面的布局
│ ├── login/page.tsx # 对应路由 /login
│ └── register/page.tsx # 对应路由 /register
├── (dashboard)/
│ ├── layout.tsx # 仪表盘布局
│ └── overview/page.tsx # 对应路由 /overview
├── _components/ # 私有文件夹(非路由)
├── layout.tsx # 根布局(必填)
└── page.tsx # 对应路由 /Anti-Patterns
反模式
❌ Fetch in Client Component when it could be Server
❌ 可在服务端完成时仍在Client Component中获取数据
typescript
// ❌ Unnecessary
'use client';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ Direct Server Component
async function UserList() {
const users = await db.users.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}typescript
// ❌ 不必要的写法
'use client';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ 直接使用Server Component
async function UserList() {
const users = await db.users.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}❌ 'use client' in layout or page
❌ 在布局或页面中添加'use client'
typescript
// ❌ Makes the entire tree client-side
'use client';
export default function Layout({ children }) { /* ... */ }
// ✅ Isolate the client component
export default function Layout({ children }) {
return <div><NavBar />{children}</div>; // NavBar can be 'use client'
}typescript
// ❌ 会导致整个组件树转为客户端渲染
'use client';
export default function Layout({ children }) { /* ... */ }
// ✅ 隔离客户端组件
export default function Layout({ children }) {
return <div><NavBar />{children}</div>; // NavBar可以标记为'use client'
}Quick Reference
速查手册
| Task | Pattern |
|---|---|
| DB in component | Server Component + async/await |
| Form | |
| Invalidate cache | |
| Redirect | |
| URL params | |
| Search params | |
| Protect routes | |
| Prevent client bundle | |
| 任务 | 实现模式 |
|---|---|
| 在组件中访问数据库 | Server Component + async/await |
| 表单实现 | |
| 失效缓存 | |
| 页面重定向 | |
| URL参数 | |
| 查询参数 | 在Server Component中使用 |
| 路由保护 | 在根目录创建 |
| 阻止客户端打包 | |
Rules
规则
- Server Components are the default; add only when the component requires browser APIs, event handlers, or React state
'use client' - Never add to layout or page files — this forces the entire subtree client-side and defeats Server Component benefits
'use client' - Server Actions () must be the mechanism for mutations from forms; avoid client-side fetch for form submissions
'use server' - or
revalidatePathmust be called after mutations that change cached data; stale caches are a correctness bugrevalidateTag - must be added to any module that accesses secrets, databases, or server-only APIs to prevent accidental client bundling
import 'server-only'
- Server Components为默认选项;仅当组件需要浏览器API、事件处理器或React状态时,才添加指令
'use client' - 切勿在布局或页面文件中添加——这会强制整个子树转为客户端渲染,丧失Server Component的优势
'use client' - 表单提交的变更操作必须通过Server Actions()实现;避免使用客户端fetch提交表单
'use server' - 在变更缓存数据后,必须调用或
revalidatePath;缓存过期会导致正确性问题revalidateTag - 任何访问密钥、数据库或仅服务端API的模块,必须添加,防止意外被打包到客户端
import 'server-only'