📖 核心概念
- Next.js全栈架构
- Server Components数据直出
- Server Actions深入
- 中间件与路由拦截
- 流式渲染与Suspense
- 错误处理与加载状态
💻 代码实现
Next.js全栈应用
✅ tsx
// middleware.ts - 路由中间件
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const { pathname } = request.nextUrl;
// 未登录用户访问受保护路由
if (pathname.startsWith('/dashboard') && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// 已登录用户访问登录页
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// 添加安全头
const response = NextResponse.next();
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
// app/dashboard/page.tsx - 流式渲染
import { Suspense } from 'react';
async function StatsCards() {
const stats = await fetchStats(); // 可能很慢
return (
<div className="grid grid-cols-3 gap-4">
{stats.map(stat => <StatCard key={stat.label} {...stat} />)}
</div>
);
}
async function RecentOrders() {
const orders = await fetchRecentOrders(); // 可能很慢
return <OrderList orders={orders} />;
}
export default function DashboardPage() {
return (
<div>
<h1>控制台</h1>
{/* 各部分独立加载,不互相阻塞 */}
<Suspense fallback={<StatsSkeleton />}>
<StatsCards />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
Server Actions与错误处理
✅ tsx
// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
// 带验证的Server Action
const updateProfileSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
bio: z.string().max(500).optional(),
});
export async function updateProfile(prevState: any, formData: FormData) {
const validated = updateProfileSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
bio: formData.get('bio'),
});
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
try {
await db.user.update({
where: { id: getCurrentUserId() },
data: validated.data,
});
revalidatePath('/dashboard/profile');
return { success: true, message: '个人资料已更新' };
} catch (error) {
return { error: { _form: ['更新失败,请重试'] } };
}
}
// app/dashboard/profile/page.tsx
'use client';
import { useActionState } from 'react';
import { updateProfile } from '@/app/actions';
function ProfileForm({ user }: { user: User }) {
const [state, formAction, isPending] = useActionState(updateProfile, null);
return (
<form action={formAction}>
{state?.error?._form && <div className="error">{state.error._form[0]}</div>}
{state?.success && <div className="success">{state.message}</div>}
<input name="name" defaultValue={user.name} />
{state?.error?.name && <span className="error">{state.error.name[0]}</span>}
<input name="email" defaultValue={user.email} type="email" />
{state?.error?.email && <span className="error">{state.error.email[0]}</span>}
<textarea name="bio" defaultValue={user.bio} />
<button type="submit" disabled={isPending}>
{isPending ? '保存中...' : '保存'}
</button>
</form>
);
}
// error.tsx - 错误边界
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div className="error-page">
<h2>出了点问题</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}
// loading.tsx - 加载状态
export default function Loading() {
return (
<div className="loading-skeleton">
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line short" />
</div>
);
}
🎯 练习任务
- 1 创建一个带认证的Next.js全栈应用
- 2 实现流式渲染,让慢数据不阻塞页面
- 3 使用Server Actions实现表单提交与乐观更新
- 4 添加中间件实现路由权限控制
🏆 成就解锁
全栈Next.js开发者 — 掌握Next.js全栈开发,一个框架搞定一切