nextjs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js Development Guidelines
Next.js 开发指南
Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.
基于Next.js 15+ App Router、Server Components和现代数据获取方式的开发模式。
Core Principles
核心原则
- Server-First Architecture: Default to Server Components, use Client Components only when needed
- File-Based Routing: Use App Router conventions for pages, layouts, and route handlers
- Data Fetching: Fetch data where it's needed using async/await in Server Components
- Type Safety: Leverage TypeScript for route params, search params, and data types
- Performance: Optimize with streaming, parallel data fetching, and static generation
- 优先服务端架构:默认使用Server Components,仅在必要时使用Client Components
- 基于文件的路由:遵循App Router约定来创建页面、布局和路由处理器
- 数据获取:在Server Components中使用async/await在需要的地方获取数据
- 类型安全:利用TypeScript确保路由参数、搜索参数和数据类型的安全性
- 性能优化:通过流式传输、并行数据获取和静态生成来优化性能
App Router Structure
App Router 结构
File Conventions
文件约定
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── posts/
│ ├── layout.tsx # Posts layout
│ ├── page.tsx # /posts
│ ├── [id]/
│ │ └── page.tsx # /posts/123
│ └── new/
│ └── page.tsx # /posts/new
└── api/
└── posts/
└── route.ts # API route handlerapp/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── posts/
│ ├── layout.tsx # Posts layout
│ ├── page.tsx # /posts
│ ├── [id]/
│ │ └── page.tsx # /posts/123
│ └── new/
│ └── page.tsx # /posts/new
└── api/
└── posts/
└── route.ts # API route handlerPage Component
页面组件
typescript
// app/posts/page.tsx
import { getPosts } from '@/lib/api';
export const metadata = {
title: 'Posts',
description: 'Browse all blog posts'
};
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}typescript
// app/posts/page.tsx
import { getPosts } from '@/lib/api';
export const metadata = {
title: 'Posts',
description: 'Browse all blog posts'
};
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}Dynamic Routes
动态路由
typescript
// app/posts/[id]/page.tsx
import { getPost } from '@/lib/api';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}typescript
// app/posts/[id]/page.tsx
import { getPost } from '@/lib/api';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Server vs Client Components
Server 组件 vs Client 组件
Server Components (Default)
Server Components(默认)
Use for:
- Data fetching
- Accessing backend resources
- Keeping sensitive info on server
- Reducing client-side JavaScript
typescript
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function PostsPage() {
// Direct database access
const posts = await db.post.findMany();
return <PostList posts={posts} />;
}适用场景:
- 数据获取
- 访问后端资源
- 敏感信息保留在服务端
- 减少客户端JavaScript体积
typescript
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function PostsPage() {
// Direct database access
const posts = await db.post.findMany();
return <PostList posts={posts} />;
}Client Components
Client Components
Use for:
- Event listeners (onClick, onChange, etc.)
- State and lifecycle (useState, useEffect)
- Browser-only APIs
- Custom hooks
typescript
// components/SearchBar.tsx
'use client'; // Required directive
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function SearchBar() {
const [query, setQuery] = useState('');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
router.push(`/search?q=${query}`);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search posts..."
/>
<button type="submit">Search</button>
</form>
);
}适用场景:
- 事件监听器(onClick、onChange等)
- 状态与生命周期(useState、useEffect)
- 仅浏览器可用的API
- 自定义hooks
typescript
// components/SearchBar.tsx
'use client'; // Required directive
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function SearchBar() {
const [query, setQuery] = useState('');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
router.push(`/search?q=${query}`);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search posts..."
/>
<button type="submit">Search</button>
</form>
);
}Composition Pattern
组合模式
typescript
// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar'; // Client Component
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<SearchBar /> {/* Client Component for interactivity */}
<PostList posts={posts} /> {/* Can be Server Component */}
</div>
);
}typescript
// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar'; // Client Component
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<SearchBar /> {/* Client Component for interactivity */}
<PostList posts={posts} /> {/* Can be Server Component */}
</div>
);
}Data Fetching
数据获取
Basic Pattern
基础模式
typescript
// Server Component with async/await
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return <PostList posts={posts} />;
}typescript
// Server Component with async/await
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return <PostList posts={posts} />;
}Parallel Data Fetching
并行数据获取
typescript
export default async function DashboardPage() {
// Fetch in parallel
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<Stats data={stats} />
</div>
);
}typescript
export default async function DashboardPage() {
// Fetch in parallel
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<Stats data={stats} />
</div>
);
}Streaming with Suspense
流式传输与Suspense
typescript
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Slow data fetch
return <PostList posts={posts} />;
}typescript
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Slow data fetch
return <PostList posts={posts} />;
}Layouts
布局
Root Layout (Required)
根布局(必填)
typescript
// app/layout.tsx
import './globals.css';
export const metadata = {
title: {
default: 'My Blog',
template: '%s | My Blog'
}
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}typescript
// app/layout.tsx
import './globals.css';
export const metadata = {
title: {
default: 'My Blog',
template: '%s | My Blog'
}
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}Nested Layout
嵌套布局
typescript
// app/posts/layout.tsx
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside>
<PostsSidebar />
</aside>
<div>{children}</div>
</div>
);
}typescript
// app/posts/layout.tsx
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside>
<PostsSidebar />
</aside>
<div>{children}</div>
</div>
);
}Route Handlers (API Routes)
路由处理器(API路由)
Basic Handler
基础处理器
typescript
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await getPosts();
return NextResponse.json({ posts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json({ post }, { status: 201 });
}typescript
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await getPosts();
return NextResponse.json({ posts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json({ post }, { status: 201 });
}Dynamic Route Handler
动态路由处理器
typescript
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({ post });
}typescript
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({ post });
}Server Actions
Server Actions
Basic Server Action
基础Server Action
typescript
// app/actions/posts.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;
const content = formData.get('content') as string;
const post = await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}typescript
// app/actions/posts.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;
const content = formData.get('content') as string;
const post = await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}Using in Forms
在表单中使用
typescript
import { createPost } from '@/app/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}typescript
import { createPost } from '@/app/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}Navigation
导航
Link Component
Link组件
typescript
import Link from 'next/link';
export function PostCard({ post }: { post: Post }) {
return (
<Link href={`/posts/${post.id}`} prefetch={true}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</Link>
);
}typescript
import Link from 'next/link';
export function PostCard({ post }: { post: Post }) {
return (
<Link href={`/posts/${post.id}`} prefetch={true}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</Link>
);
}Programmatic Navigation
编程式导航
typescript
'use client';
import { useRouter } from 'next/navigation';
export function PostActions({ postId }: { postId: string }) {
const router = useRouter();
const handleDelete = async () => {
await deletePost(postId);
router.push('/posts');
router.refresh(); // Refresh server components
};
return <button onClick={handleDelete}>Delete</button>;
}typescript
'use client';
import { useRouter } from 'next/navigation';
export function PostActions({ postId }: { postId: string }) {
const router = useRouter();
const handleDelete = async () => {
await deletePost(postId);
router.push('/posts');
router.refresh(); // Refresh server components
};
return <button onClick={handleDelete}>Delete</button>;
}Router Hooks
路由Hooks
typescript
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname(); // /posts/123
const searchParams = useSearchParams(); // ?q=hello
const query = searchParams.get('q');typescript
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname(); // /posts/123
const searchParams = useSearchParams(); // ?q=hello
const query = searchParams.get('q');Metadata
元数据
typescript
// Static metadata
export const metadata = {
title: 'All Posts',
description: 'Browse our collection of blog posts'
};
// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}typescript
// Static metadata
export const metadata = {
title: 'All Posts',
description: 'Browse our collection of blog posts'
};
// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}Error Handling
错误处理
typescript
// app/posts/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/posts/[id]/not-found.tsx
export default function NotFound() {
return <div>Post Not Found</div>;
}typescript
// app/posts/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/posts/[id]/not-found.tsx
export default function NotFound() {
return <div>Post Not Found</div>;
}Static Generation & ISR
静态生成与ISR
typescript
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ id: post.id }));
}
// Revalidate every hour (ISR)
export const revalidate = 3600;
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
return <Post data={post} />;
}typescript
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ id: post.id }));
}
// Revalidate every hour (ISR)
export const revalidate = 3600;
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
return <Post data={post} />;
}Additional Resources
额外资源
For detailed information, see:
- Server Actions Guide
- Data Fetching Patterns
- Routing and Navigation
- Performance Optimization
如需详细信息,请参阅:
- Server Actions 指南
- 数据获取模式
- 路由与导航
- 性能优化