📚 现代前端开发 目录
阶段5:全栈与部署
第 30 / 35 课

Next.js全栈

一个框架搞定前后端

📖 核心概念

💻 代码实现

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>
  );
}

🎯 练习任务

🏆 成就解锁
全栈Next.js开发者 — 掌握Next.js全栈开发,一个框架搞定一切