nextjs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js
Next.js
Modern Next.js development with App Router following industry best practices. This skill covers Server Components, data fetching, API routes, middleware, authentication, and full-stack patterns.
遵循行业最佳实践的现代化Next.js开发(基于App Router)。本内容涵盖Server Components、数据获取、API路由、中间件、身份验证及全栈开发模式。
Purpose
用途
Build production-ready Next.js applications:
- Master App Router architecture
- Implement Server and Client Components
- Handle data fetching and caching
- Create type-safe API routes
- Implement authentication and middleware
- Optimize performance and SEO
构建可投入生产环境的Next.js应用:
- 掌握App Router架构
- 实现Server与Client Components
- 处理数据获取与缓存
- 创建类型安全的API路由
- 实现身份验证与中间件
- 优化性能与SEO
Features
特性
1. App Router Structure
1. App Router 结构
typescript
// app/layout.tsx - Root layout
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'A Next.js application',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
// app/page.tsx - Home page
import { Suspense } from 'react';
export default function HomePage() {
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold">Featured Products</h1>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
</div>
);
}
// app/products/[id]/page.tsx - Dynamic route
import { notFound } from 'next/navigation';
interface ProductPageProps {
params: { id: string };
}
export async function generateMetadata({ params }: ProductPageProps) {
const product = await getProduct(params.id);
if (!product) return { title: 'Not Found' };
return { title: product.name, description: product.description };
}
export default async function ProductPage({ params }: ProductPageProps) {
const product = await getProduct(params.id);
if (!product) notFound();
return <ProductDetails product={product} />;
}typescript
// app/layout.tsx - Root layout
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'A Next.js application',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
// app/page.tsx - Home page
import { Suspense } from 'react';
export default function HomePage() {
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold">Featured Products</h1>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
</div>
);
}
// app/products/[id]/page.tsx - Dynamic route
import { notFound } from 'next/navigation';
interface ProductPageProps {
params: { id: string };
}
export async function generateMetadata({ params }: ProductPageProps) {
const product = await getProduct(params.id);
if (!product) return { title: 'Not Found' };
return { title: product.name, description: product.description };
}
export default async function ProductPage({ params }: ProductPageProps) {
const product = await getProduct(params.id);
if (!product) notFound();
return <ProductDetails product={product} />;
}2. Server and Client Components
2. Server与Client Components
typescript
// Server Component (default)
// app/components/product-list.tsx
import { getProducts } from '@/lib/products';
export async function ProductList() {
const products = await getProducts();
return (
<div className="grid grid-cols-3 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Client Component
// app/components/add-to-cart-button.tsx
'use client';
import { useState, useTransition } from 'react';
import { addToCart } from '@/lib/actions';
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
const [message, setMessage] = useState<string | null>(null);
const handleClick = () => {
startTransition(async () => {
const result = await addToCart(productId);
setMessage(result.message);
});
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
);
}typescript
// Server Component (default)
// app/components/product-list.tsx
import { getProducts } from '@/lib/products';
export async function ProductList() {
const products = await getProducts();
return (
<div className="grid grid-cols-3 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Client Component
// app/components/add-to-cart-button.tsx
'use client';
import { useState, useTransition } from 'react';
import { addToCart } from '@/lib/actions';
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
const [message, setMessage] = useState<string | null>(null);
const handleClick = () => {
startTransition(async () => {
const result = await addToCart(productId);
setMessage(result.message);
});
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
);
}3. Server Actions
3. Server Actions
typescript
// app/lib/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { auth } from '@/lib/auth';
const createProductSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
description: z.string().optional(),
});
export async function createProduct(formData: FormData) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
const validatedFields = createProductSchema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
description: formData.get('description'),
});
if (!validatedFields.success) {
return { success: false, errors: validatedFields.error.flatten().fieldErrors };
}
const product = await db.product.create({
data: { ...validatedFields.data, userId: session.user.id },
});
revalidatePath('/products');
redirect(`/products/${product.id}`);
}
export async function addToCart(productId: string) {
const session = await auth();
if (!session?.user?.id) {
return { success: false, message: 'Please sign in' };
}
await db.cartItem.upsert({
where: { userId_productId: { userId: session.user.id, productId } },
update: { quantity: { increment: 1 } },
create: { userId: session.user.id, productId, quantity: 1 },
});
revalidatePath('/cart');
return { success: true, message: 'Added to cart!' };
}typescript
// app/lib/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { auth } from '@/lib/auth';
const createProductSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
description: z.string().optional(),
});
export async function createProduct(formData: FormData) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
const validatedFields = createProductSchema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
description: formData.get('description'),
});
if (!validatedFields.success) {
return { success: false, errors: validatedFields.error.flatten().fieldErrors };
}
const product = await db.product.create({
data: { ...validatedFields.data, userId: session.user.id },
});
revalidatePath('/products');
redirect(`/products/${product.id}`);
}
export async function addToCart(productId: string) {
const session = await auth();
if (!session?.user?.id) {
return { success: false, message: 'Please sign in' };
}
await db.cartItem.upsert({
where: { userId_productId: { userId: session.user.id, productId } },
update: { quantity: { increment: 1 } },
create: { userId: session.user.id, productId, quantity: 1 },
});
revalidatePath('/cart');
return { success: true, message: 'Added to cart!' };
}4. Data Fetching and Caching
4. 数据获取与缓存
typescript
// Cached data fetching
import { unstable_cache } from 'next/cache';
export const getProducts = unstable_cache(
async () => {
return db.product.findMany({
include: { category: true },
orderBy: { createdAt: 'desc' },
});
},
['products'],
{ tags: ['products'], revalidate: 3600 }
);
// Fetch with built-in caching
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 3600 },
});
return res.json();
}
// No caching for dynamic data
async function getCurrentPrice(symbol: string) {
const res = await fetch(`https://api.example.com/stocks/${symbol}`, {
cache: 'no-store',
});
return res.json();
}
// Parallel data fetching
export async function getProductPageData(id: string) {
const [product, relatedProducts, reviews] = await Promise.all([
getProduct(id),
getRelatedProducts(id),
getProductReviews(id),
]);
return { product, relatedProducts, reviews };
}typescript
// Cached data fetching
import { unstable_cache } from 'next/cache';
export const getProducts = unstable_cache(
async () => {
return db.product.findMany({
include: { category: true },
orderBy: { createdAt: 'desc' },
});
},
['products'],
{ tags: ['products'], revalidate: 3600 }
);
// Fetch with built-in caching
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 3600 },
});
return res.json();
}
// No caching for dynamic data
async function getCurrentPrice(symbol: string) {
const res = await fetch(`https://api.example.com/stocks/${symbol}`, {
cache: 'no-store',
});
return res.json();
}
// Parallel data fetching
export async function getProductPageData(id: string) {
const [product, relatedProducts, reviews] = await Promise.all([
getProduct(id),
getRelatedProducts(id),
getProductReviews(id),
]);
return { product, relatedProducts, reviews };
}5. API Routes
5. API路由
typescript
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get('page') || '1');
const limit = Number(searchParams.get('limit') || '10');
const [products, total] = await Promise.all([
db.product.findMany({ skip: (page - 1) * limit, take: limit }),
db.product.count(),
]);
return NextResponse.json({
data: products,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
});
}
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const product = await db.product.create({
data: { ...body, userId: session.user.id },
});
return NextResponse.json(product, { status: 201 });
}
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(product);
}typescript
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get('page') || '1');
const limit = Number(searchParams.get('limit') || '10');
const [products, total] = await Promise.all([
db.product.findMany({ skip: (page - 1) * limit, take: limit }),
db.product.count(),
]);
return NextResponse.json({
data: products,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
});
}
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const product = await db.product.create({
data: { ...body, userId: session.user.id },
});
return NextResponse.json(product, { status: 201 });
}
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(product);
}6. Middleware
6. 中间件
typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { auth } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Public routes
const publicRoutes = ['/', '/login', '/register'];
if (publicRoutes.some((route) => pathname.startsWith(route))) {
return NextResponse.next();
}
// Check authentication
const session = await auth();
if (!session) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// Admin-only routes
if (pathname.startsWith('/admin') && session.user.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { auth } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Public routes
const publicRoutes = ['/', '/login', '/register'];
if (publicRoutes.some((route) => pathname.startsWith(route))) {
return NextResponse.next();
}
// Check authentication
const session = await auth();
if (!session) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// Admin-only routes
if (pathname.startsWith('/admin') && session.user.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};7. Error Handling
7. 错误处理
typescript
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold">Page Not Found</h2>
<Link href="/">Return Home</Link>
</div>
);
}typescript
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold">Page Not Found</h2>
<Link href="/">Return Home</Link>
</div>
);
}8. Performance Optimization
8. 性能优化
typescript
// Image optimization
import Image from 'next/image';
export function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
placeholder="blur"
/>
);
}
// Dynamic imports
import dynamic from 'next/dynamic';
const DynamicChart = dynamic(() => import('./Chart'), {
loading: () => <p>Loading...</p>,
ssr: false,
});
// Static generation
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({ id: product.id }));
}typescript
// Image optimization
import Image from 'next/image';
export function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
placeholder="blur"
/>
);
}
// Dynamic imports
import dynamic from 'next/dynamic';
const DynamicChart = dynamic(() => import('./Chart'), {
loading: () => <p>Loading...</p>,
ssr: false,
});
// Static generation
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({ id: product.id }));
}Use Cases
应用场景
E-commerce Product Page
电商产品页面
typescript
export default async function ProductPage({ params, searchParams }) {
const product = await getProductBySlug(params.slug);
if (!product) notFound();
const selectedVariant = product.variants.find(
(v) => v.id === searchParams.variant
) || product.variants[0];
return (
<div className="grid md:grid-cols-2 gap-8">
<ProductGallery images={product.images} />
<div>
<h1>{product.name}</h1>
<p>${selectedVariant.price}</p>
<VariantSelector variants={product.variants} />
<AddToCartButton productId={product.id} />
</div>
</div>
);
}typescript
export default async function ProductPage({ params, searchParams }) {
const product = await getProductBySlug(params.slug);
if (!product) notFound();
const selectedVariant = product.variants.find(
(v) => v.id === searchParams.variant
) || product.variants[0];
return (
<div className="grid md:grid-cols-2 gap-8">
<ProductGallery images={product.images} />
<div>
<h1>{product.name}</h1>
<p>${selectedVariant.price}</p>
<VariantSelector variants={product.variants} />
<AddToCartButton productId={product.id} />
</div>
</div>
);
}Best Practices
最佳实践
Do's
建议
- Use Server Components by default
- Colocate data fetching with components
- Implement loading and error states
- Use Server Actions for mutations
- Cache data appropriately
- Optimize images with next/image
- 默认使用Server Components
- 将数据获取与组件放在同一位置
- 实现加载与错误状态
- 使用Server Actions处理数据变更
- 合理缓存数据
- 使用next/image优化图片
Don'ts
避免
- Don't use 'use client' unnecessarily
- Don't fetch data in Client Components
- Don't ignore TypeScript errors
- Don't skip error boundaries
- Don't hardcode environment variables
- 不必要地使用'use client'
- 在Client Components中获取数据
- 忽略TypeScript错误
- 跳过错误边界处理
- 硬编码环境变量