Next.js 15全栈开发完全教程

教程简介

全面讲解Next.js 15全栈框架的核心特性与实战开发,涵盖App Router架构、React Server Components、Server Actions、流式渲染与Suspense、中间件与认证、API Routes、数据库集成(Prisma)、部署优化(Vercel/自托管)、SEO优化、性能调优等核心内容。

Next.js 15 全栈开发完全教程

关键词:Next.js, React, 全栈开发, SSR, 前端框架

本教程全面讲解 Next.js 15 的核心特性与实战开发,从 App Router 架构到生产部署,带你构建高性能的现代 Web 应用。


目录

  1. Next.js 15 新特性概览
  2. 项目初始化与目录结构
  3. App Router 架构详解
  4. React Server Components
  5. Server Actions 与数据变更
  6. 流式渲染与 Suspense
  7. 中间件与认证
  8. API Routes 路由处理
  9. 数据库集成(Prisma)
  10. 部署优化
  11. SEO 优化
  12. 性能调优
  13. 总结与最佳实践

1. Next.js 15 新特性概览

1.1 为什么选择 Next.js

Next.js 是基于 React 的全栈框架,提供了开箱即用的服务端渲染(SSR)、静态生成(SSG)、API 路由、中间件等能力。相比纯 React SPA,Next.js 的核心优势:

  • 更快的首屏加载:服务端渲染 + 流式传输
  • 更好的 SEO:搜索引擎可以直接抓取完整 HTML
  • 全栈一体化:前后端代码在同一项目中
  • 零配置优化:自动代码分割、图片优化、字体优化
  • 灵活的渲染策略:SSR、SSG、ISR 按需选择

1.2 Next.js 15 核心更新

  • React 19 支持:完整支持 React 19 的新特性
  • Turbopack 稳定版:开发服务器速度提升 10 倍以上
  • Partial Prerendering(部分预渲染):静态壳 + 动态流式内容
  • 改进的缓存策略:更细粒度的缓存控制
  • 增强的 Server Actions:更稳定的表单处理和数据变更
  • 自定义 createNextApp:更灵活的项目脚手架

2. 项目初始化与目录结构

2.1 创建项目

# 使用官方脚手架创建
npx create-next-app@latest my-app

# 推荐选项:
# ✔ Would you like to use TypeScript? → Yes
# ✔ Would you like to use ESLint? → Yes
# ✔ Would you like to use Tailwind CSS? → Yes
# ✔ Would you like your code inside a `src/` directory? → Yes
# ✔ Would you like to use App Router? → Yes
# ✔ Would you like to use Turbopack for next dev? → Yes
# ✔ Would you like to customize the import alias? → No

cd my-app
npm run dev

2.2 项目目录结构

my-app/
├── src/
│   ├── app/                    # App Router 根目录
│   │   ├── layout.tsx          # 根布局
│   │   ├── page.tsx            # 首页
│   │   ├── loading.tsx         # 加载状态
│   │   ├── error.tsx           # 错误边界
│   │   ├── not-found.tsx       # 404 页面
│   │   ├── globals.css         # 全局样式
│   │   ├── dashboard/          # 路由分组
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   └── settings/
│   │   │       └── page.tsx
│   │   ├── blog/
│   │   │   ├── page.tsx        # 博客列表
│   │   │   └── [slug]/
│   │   │       └── page.tsx    # 动态路由
│   │   └── api/
│   │       └── users/
│   │           └── route.ts    # API 路由
│   ├── components/             # 共享组件
│   │   ├── ui/                 # 基础 UI 组件
│   │   └── features/           # 业务组件
│   ├── lib/                    # 工具函数
│   │   ├── db.ts               # 数据库连接
│   │   └── utils.ts            # 通用工具
│   └── types/                  # TypeScript 类型
├── public/                     # 静态资源
├── prisma/                     # Prisma Schema
├── next.config.ts              # Next.js 配置
├── tailwind.config.ts          # Tailwind 配置
├── tsconfig.json               # TypeScript 配置
└── package.json

2.3 配置文件

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // 图片域名白名单
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.example.com",
      },
    ],
  },
  // 实验性功能
  experimental: {
    // 启用 Partial Prerendering
    ppr: true,
  },
};

export default nextConfig;

3. App Router 架构详解

3.1 路由基础

App Router 基于文件系统路由,目录结构即路由结构:

// src/app/page.tsx → /
export default function HomePage() {
  return <h1>首页</h1>;
}

// src/app/about/page.tsx → /about
export default function AboutPage() {
  return <h1>关于我们</h1>;
}

// src/app/blog/page.tsx → /blog
export default function BlogPage() {
  return <h1>博客列表</h1>;
}

3.2 动态路由与路由组

// 动态路由:src/app/blog/[slug]/page.tsx → /blog/my-post
interface BlogPostProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPost({ params }: BlogPostProps) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// 生成静态参数(SSG)
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

// 生成元数据
export async function generateMetadata({ params }: BlogPostProps) {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

路由组(不影响 URL 结构):

src/app/
├── (marketing)/           # 营销页面组
│   ├── layout.tsx         # 营销布局
│   ├── page.tsx           # 首页
│   └── pricing/
│       └── page.tsx       # /pricing
├── (dashboard)/           # 仪表盘组
│   ├── layout.tsx         # 仪表盘布局
│   ├── dashboard/
│   │   └── page.tsx       # /dashboard
│   └── settings/
│       └── page.tsx       # /settings

3.3 并行路由与拦截路由

// 并行路由:同一页面同时渲染多个独立页面
// src/app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-3 gap-4">
      <main>{children}</main>
      <aside>{analytics}</aside>
      <aside>{team}</aside>
    </div>
  );
}

// 拦截路由:模态框场景
// src/app/(.)photo/[id]/page.tsx → 拦截 /photo/[id]
import { Modal } from "@/components/modal";
import { getPhoto } from "@/lib/api";

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const photo = await getPhoto(id);

  return (
    <Modal>
      <img src={photo.url} alt={photo.title} />
    </Modal>
  );
}

3.4 布局与模板

// src/app/dashboard/layout.tsx - 嵌套布局(导航时保持状态)
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      <Sidebar />
      <main className="flex-1 p-8">{children}</main>
    </div>
  );
}

// src/app/dashboard/template.tsx - 模板(每次导航重新挂载)
export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div className="animate-fade-in">{children}</div>;
}

4. React Server Components

4.1 Server Components vs Client Components

Next.js 15 中,所有组件默认是 Server Components,在服务端渲染,不发送 JavaScript 到客户端:

// ✅ Server Component(默认)- 在服务端运行
// 可以直接访问数据库、文件系统、环境变量
import { db } from "@/lib/db";

export default async function UserList() {
  const users = await db.user.findMany({
    orderBy: { createdAt: "desc" },
    take: 10,
  });

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} className="p-4 border-b">
          <h3 className="font-semibold">{user.name}</h3>
          <p className="text-gray-500">{user.email}</p>
        </li>
      ))}
    </ul>
  );
}
// ✅ Client Component - 在客户端运行
// 需要 "use client" 指令
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex items-center gap-4">
      <span className="text-2xl font-bold">{count}</span>
      <button
        onClick={() => setCount((c) => c + 1)}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        增加
      </button>
    </div>
  );
}

4.2 组件组合模式

最佳实践是将 Server Component 和 Client Component 组合使用:

// Server Component 作为容器,传递数据给 Client Component
import { db } from "@/lib/db";
import { SearchableList } from "@/components/searchable-list";

export default async function ProductsPage() {
  // 在服务端获取数据
  const products = await db.product.findMany();

  // 将数据传递给客户端组件进行交互
  return (
    <div>
      <h1>产品列表</h1>
      <SearchableList items={products} />
    </div>
  );
}
// Client Component 负责交互逻辑
"use client";

import { useState } from "react";

interface Product {
  id: string;
  name: string;
  price: number;
}

export function SearchableList({ items }: { items: Product[] }) {
  const [query, setQuery] = useState("");

  const filtered = items.filter((item) =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        placeholder="搜索产品..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className="w-full p-2 border rounded mb-4"
      />
      <div className="grid grid-cols-3 gap-4">
        {filtered.map((item) => (
          <div key={item.id} className="p-4 border rounded">
            <h3>{item.name}</h3>
            <p className="text-green-600">¥{item.price}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

5. Server Actions 与数据变更

5.1 定义 Server Actions

Server Actions 是在服务端执行的异步函数,可以直接在组件中调用:

// src/app/actions.ts
"use server";

import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";

// 表单验证 Schema
const createPostSchema = z.object({
  title: z.string().min(1, "标题不能为空").max(100),
  content: z.string().min(10, "内容至少 10 个字符"),
  category: z.enum(["tech", "life", "other"]),
});

export interface ActionState {
  success: boolean;
  errors?: Record<string, string[]>;
  message?: string;
}

export async function createPost(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  // 验证数据
  const validatedFields = createPostSchema.safeParse({
    title: formData.get("title"),
    content: formData.get("content"),
    category: formData.get("category"),
  });

  if (!validatedFields.success) {
    return {
      success: false,
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  try {
    await db.post.create({
      data: validatedFields.data,
    });
  } catch (error) {
    return {
      success: false,
      message: "创建失败,请稍后重试",
    };
  }

  revalidatePath("/blog");
  redirect("/blog");
}

export async function deletePost(postId: string): Promise<ActionState> {
  try {
    await db.post.delete({ where: { id: postId } });
    revalidatePath("/blog");
    return { success: true, message: "删除成功" };
  } catch (error) {
    return { success: false, message: "删除失败" };
  }
}

5.2 在组件中使用 Server Actions

// src/app/blog/create/page.tsx
"use client";

import { useActionState } from "react";
import { createPost, type ActionState } from "@/app/actions";

const initialState: ActionState = { success: false };

export default function CreatePostPage() {
  const [state, formAction, isPending] = useActionState(
    createPost,
    initialState
  );

  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-2xl font-bold mb-6">创建文章</h1>
      <form action={formAction} className="space-y-4">
        <div>
          <label htmlFor="title" className="block text-sm font-medium mb-1">
            标题
          </label>
          <input
            id="title"
            name="title"
            type="text"
            className="w-full p-2 border rounded"
          />
          {state.errors?.title && (
            <p className="text-red-500 text-sm mt-1">
              {state.errors.title[0]}
            </p>
          )}
        </div>

        <div>
          <label htmlFor="content" className="block text-sm font-medium mb-1">
            内容
          </label>
          <textarea
            id="content"
            name="content"
            rows={8}
            className="w-full p-2 border rounded"
          />
          {state.errors?.content && (
            <p className="text-red-500 text-sm mt-1">
              {state.errors.content[0]}
            </p>
          )}
        </div>

        <div>
          <label htmlFor="category" className="block text-sm font-medium mb-1">
            分类
          </label>
          <select
            id="category"
            name="category"
            className="w-full p-2 border rounded"
          >
            <option value="tech">技术</option>
            <option value="life">生活</option>
            <option value="other">其他</option>
          </select>
        </div>

        {state.message && (
          <p className="text-red-500 text-sm">{state.message}</p>
        )}

        <button
          type="submit"
          disabled={isPending}
          className="w-full py-2 bg-blue-600 text-white rounded disabled:opacity-50"
        >
          {isPending ? "提交中..." : "发布文章"}
        </button>
      </form>
    </div>
  );
}

6. 流式渲染与 Suspense

6.1 流式渲染原理

Next.js 支持流式渲染,页面可以分块传输到浏览器,用户不必等待所有数据加载完成即可看到页面:

// src/app/dashboard/page.tsx
import { Suspense } from "react";
import { RevenueChart } from "@/components/revenue-chart";
import { LatestInvoices } from "@/components/latest-invoices";
import { DashboardSkeleton } from "@/components/skeletons";

export default function DashboardPage() {
  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold mb-6">仪表盘</h1>

      {/* 静态内容立即显示 */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        {/* 图表组件独立加载,不影响其他部分 */}
        <Suspense fallback={<DashboardSkeleton />}>
          <RevenueChart />
        </Suspense>

        <Suspense fallback={<DashboardSkeleton />}>
          <LatestInvoices />
        </Suspense>
      </div>
    </main>
  );
}

6.2 Loading UI

使用 loading.tsx 自动创建加载状态:

// src/app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="p-8 animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-48 mb-6" />
      <div className="grid grid-cols-2 gap-6">
        <div className="h-64 bg-gray-200 rounded" />
        <div className="h-64 bg-gray-200 rounded" />
      </div>
    </div>
  );
}

6.3 错误处理

// src/app/dashboard/error.tsx
"use client";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <h2 className="text-xl font-semibold mb-4">出错了</h2>
      <p className="text-gray-500 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        重试
      </button>
    </div>
  );
}

7. 中间件与认证

7.1 中间件基础

中间件在请求完成前运行,可以实现重定向、认证检查、请求头修改等:

// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// 需要认证的路径
const protectedRoutes = ["/dashboard", "/settings", "/profile"];
// 已登录用户不应访问的路径
const authRoutes = ["/login", "/register"];

export function middleware(request: NextRequest) {
  const token = request.cookies.get("session")?.value;
  const { pathname } = request.nextUrl;

  // 检查是否是受保护的路由
  const isProtectedRoute = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  );
  const isAuthRoute = authRoutes.some((route) => pathname.startsWith(route));

  // 未登录访问受保护路由 → 重定向到登录页
  if (isProtectedRoute && !token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 已登录访问登录页 → 重定向到仪表盘
  if (isAuthRoute && token) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  // 设置安全头
  const response = NextResponse.next();
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "origin-when-cross-origin");

  return response;
}

export const config = {
  matcher: [
    // 匹配所有路径,排除静态资源和 API
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

7.2 完整认证方案

// src/lib/auth.ts
import { cookies } from "next/headers";
import { db } from "./db";
import { SignJWT, jwtVerify } from "jose";

const secretKey = new TextEncoder().encode(
  process.env.JWT_SECRET ?? "default-secret"
);

export async function createSession(userId: string) {
  const token = await new SignJWT({ userId })
    .setProtectedHeader({ alg: "HS256" })
    .setExpirationTime("7d")
    .sign(secretKey);

  const cookieStore = await cookies();
  cookieStore.set("session", token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7 天
    path: "/",
  });
}

export async function getSession() {
  const cookieStore = await cookies();
  const token = cookieStore.get("session")?.value;
  if (!token) return null;

  try {
    const { payload } = await jwtVerify(token, secretKey);
    return payload as { userId: string };
  } catch {
    return null;
  }
}

export async function getCurrentUser() {
  const session = await getSession();
  if (!session) return null;

  return db.user.findUnique({
    where: { id: session.userId },
    select: { id: true, name: true, email: true, avatar: true },
  });
}

export async function deleteSession() {
  const cookieStore = await cookies();
  cookieStore.delete("session");
}

8. API Routes 路由处理

8.1 RESTful API

// src/app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getSession } from "@/lib/auth";
import { z } from "zod";

const userSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

// GET /api/users
export async function GET(request: NextRequest) {
  const session = await getSession();
  if (!session) {
    return NextResponse.json({ error: "未授权" }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get("page") ?? "1");
  const limit = parseInt(searchParams.get("limit") ?? "10");

  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
    orderBy: { createdAt: "desc" },
    select: { id: true, name: true, email: true, createdAt: true },
  });

  const total = await db.user.count();

  return NextResponse.json({
    data: users,
    pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
  });
}

// POST /api/users
export async function POST(request: NextRequest) {
  const session = await getSession();
  if (!session) {
    return NextResponse.json({ error: "未授权" }, { status: 401 });
  }

  const body = await request.json();
  const validatedData = userSchema.safeParse(body);

  if (!validatedData.success) {
    return NextResponse.json(
      { error: validatedData.error.flatten().fieldErrors },
      { status: 400 }
    );
  }

  try {
    const user = await db.user.create({
      data: validatedData.data,
    });
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: "创建用户失败" },
      { status: 500 }
    );
  }
}
// src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";

// GET /api/users/:id
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const user = await db.user.findUnique({ where: { id } });

  if (!user) {
    return NextResponse.json({ error: "用户不存在" }, { status: 404 });
  }
  return NextResponse.json(user);
}

// PATCH /api/users/:id
export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const body = await request.json();

  const user = await db.user.update({
    where: { id },
    data: body,
  });
  return NextResponse.json(user);
}

// DELETE /api/users/:id
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await db.user.delete({ where: { id } });
  return NextResponse.json({ message: "删除成功" });
}

8.2 路由处理工具函数

// src/lib/api-utils.ts
import { NextResponse } from "next/server";
import { ZodSchema } from "zod";

export function successResponse<T>(data: T, status = 200) {
  return NextResponse.json({ success: true, data }, { status });
}

export function errorResponse(message: string, status = 400) {
  return NextResponse.json({ success: false, error: message }, { status });
}

export async function validateRequest<T>(
  request: Request,
  schema: ZodSchema<T>
): Promise<{ success: true; data: T } | { success: false; errors: Record<string, string[]> }> {
  const body = await request.json();
  const result = schema.safeParse(body);

  if (result.success) {
    return { success: true, data: result.data };
  }
  return {
    success: false,
    errors: result.error.flatten().fieldErrors as Record<string, string[]>,
  };
}

9. 数据库集成(Prisma)

9.1 Prisma 设置

# 安装 Prisma
npm install prisma @prisma/client
npx prisma init
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  avatar    String?
  posts     Post[]
  comments  Comment[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Post {
  id        String    @id @default(cuid())
  title     String
  slug      String    @unique
  content   String
  excerpt   String?
  published Boolean   @default(false)
  category  String
  author    User      @relation(fields: [authorId], references: [id])
  authorId  String
  comments  Comment[]
  tags      Tag[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@index([slug])
  @@index([authorId])
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())

  @@index([postId])
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]
}
# 生成客户端并推送数据库
npx prisma generate
npx prisma db push

# 或使用迁移
npx prisma migrate dev --name init

9.2 数据库连接单例

// src/lib/db.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query"] : [],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = db;
}

9.3 数据查询示例

// src/lib/queries.ts
import { db } from "./db";

// 带分页和搜索的文章列表
export async function getPosts({
  page = 1,
  limit = 10,
  search,
  category,
}: {
  page?: number;
  limit?: number;
  search?: string;
  category?: string;
}) {
  const where = {
    published: true,
    ...(search && {
      OR: [
        { title: { contains: search, mode: "insensitive" as const } },
        { content: { contains: search, mode: "insensitive" as const } },
      ],
    }),
    ...(category && { category }),
  };

  const [posts, total] = await Promise.all([
    db.post.findMany({
      where,
      include: {
        author: { select: { name: true, avatar: true } },
        tags: { select: { name: true } },
        _count: { select: { comments: true } },
      },
      orderBy: { createdAt: "desc" },
      skip: (page - 1) * limit,
      take: limit,
    }),
    db.post.count({ where }),
  ]);

  return {
    posts,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

// 获取单篇文章(含评论)
export async function getPostBySlug(slug: string) {
  return db.post.findUnique({
    where: { slug },
    include: {
      author: { select: { id: true, name: true, avatar: true } },
      tags: true,
      comments: {
        include: {
          author: { select: { name: true, avatar: true } },
        },
        orderBy: { createdAt: "desc" },
      },
    },
  });
}

10. 部署优化

10.1 Vercel 部署

Vercel 是 Next.js 的官方托管平台,部署最简单:

# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

# 部署到生产环境
vercel --prod

vercel.json 配置:

{
  "framework": "nextjs",
  "buildCommand": "next build",
  "regions": ["hkg1", "sin1"],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "s-maxage=60, stale-while-revalidate" }
      ]
    }
  ]
}

10.2 Docker 自托管

# Dockerfile
FROM node:20-alpine AS base

# 安装依赖
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# 构建
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# 生产运行
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
// next.config.ts 中启用 standalone 输出
const nextConfig: NextConfig = {
  output: "standalone",
};
# 构建并运行
docker build -t my-next-app .
docker run -p 3000:3000 my-next-app

10.3 环境变量管理

# .env.local(本地开发,不提交到 git)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=your-secret-key
NEXT_PUBLIC_API_URL=http://localhost:3000/api

# .env.production(生产环境)
DATABASE_URL=postgresql://user:pass@prod-db:5432/mydb
JWT_SECRET=production-secret
NEXT_PUBLIC_API_URL=https://myapp.com/api
// src/lib/env.ts - 类型安全的环境变量
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NEXT_PUBLIC_API_URL: z.string().url(),
});

export const env = envSchema.parse(process.env);

11. SEO 优化

11.1 元数据 API

// src/app/layout.tsx - 全局元数据
import type { Metadata } from "next";

export const metadata: Metadata = {
  metadataBase: new URL("https://myapp.com"),
  title: {
    default: "我的应用",
    template: "%s | 我的应用",
  },
  description: "一个使用 Next.js 15 构建的现代 Web 应用",
  keywords: ["Next.js", "React", "全栈开发"],
  openGraph: {
    type: "website",
    locale: "zh_CN",
    siteName: "我的应用",
  },
  robots: {
    index: true,
    follow: true,
  },
};

// src/app/blog/[slug]/page.tsx - 动态元数据
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  if (!post) return { title: "文章不存在" };

  return {
    title: post.title,
    description: post.excerpt ?? post.content.slice(0, 160),
    openGraph: {
      title: post.title,
      description: post.excerpt ?? "",
      type: "article",
      publishedTime: post.createdAt.toISOString(),
      authors: [post.author.name],
      images: [{ url: post.coverImage ?? "/og-default.png" }],
    },
  };
}

11.2 Sitemap 与 Robots

// src/app/sitemap.ts
import type { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();

  const postUrls = posts.map((post) => ({
    url: `https://myapp.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));

  return [
    { url: "https://myapp.com", lastModified: new Date(), priority: 1 },
    { url: "https://myapp.com/blog", lastModified: new Date(), priority: 0.9 },
    ...postUrls,
  ];
}

// src/app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: "*", allow: "/" },
      { userAgent: "*", disallow: ["/api/", "/dashboard/"] },
    ],
    sitemap: "https://myapp.com/sitemap.xml",
  };
}

11.3 结构化数据

// src/app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    datePublished: post.createdAt.toISOString(),
    dateModified: post.updatedAt.toISOString(),
    author: {
      "@type": "Person",
      name: post.author.name,
    },
    description: post.excerpt,
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article className="max-w-3xl mx-auto p-8">
        <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
        {/* 文章内容 */}
      </article>
    </>
  );
}

12. 性能调优

12.1 图片优化

import Image from "next/image";

export function OptimizedImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority            // 首屏图片使用 priority
      placeholder="blur"  // 加载时显示模糊占位图
      blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
      sizes="(max-width: 768px) 100vw, 1200px"
    />
  );
}

12.2 字体优化

// src/app/layout.tsx
import { Inter, Noto_Sans_SC } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
});

const notoSansSC = Noto_Sans_SC({
  subsets: ["latin"],
  weight: ["400", "500", "700"],
  variable: "--font-noto-sans-sc",
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN" className={`${inter.variable} ${notoSansSC.variable}`}>
      <body className="font-sans antialiased">{children}</body>
    </html>
  );
}

12.3 数据缓存策略

import { unstable_cache } from "next/cache";

// 缓存数据库查询,60 秒内复用
const getCachedPosts = unstable_cache(
  async () => {
    return db.post.findMany({
      where: { published: true },
      orderBy: { createdAt: "desc" },
      take: 20,
    });
  },
  ["posts-list"],
  { revalidate: 60, tags: ["posts"] }
);

// Server Action 中按标签精确清除缓存
"use server";
import { revalidateTag } from "next/cache";

export async function createPost(data: PostInput) {
  await db.post.create({ data });
  revalidateTag("posts");  // 只清除文章相关缓存
}

12.4 代码分割与懒加载

"use client";

import dynamic from "next/dynamic";

// 懒加载重量级组件
const MarkdownEditor = dynamic(() => import("@/components/markdown-editor"), {
  loading: () => <div className="h-96 bg-gray-100 animate-pulse rounded" />,
  ssr: false,  // 仅客户端渲染
});

// 条件加载
export function PostEditor() {
  const [showEditor, setShowEditor] = useState(false);

  return (
    <div>
      <button onClick={() => setShowEditor(true)}>开始编辑</button>
      {showEditor && <MarkdownEditor />}
    </div>
  );
}

13. 总结与最佳实践

13.1 项目结构建议

  • 使用 src/ 目录组织代码,保持清晰的层次
  • 按功能而非类型组织组件(features/ 而非 components/ + hooks/ + utils/
  • 共享组件放在 components/ui/,业务组件放在 components/features/

13.2 Server/Client 组件选择

  • 默认使用 Server Component:数据获取、纯展示组件
  • 仅在需要时添加 "use client":交互逻辑、浏览器 API、事件处理
  • 保持 Client 组件尽量小:将交互部分从大组件中抽离

13.3 数据获取最佳实践

  • Server Component 中直接使用 async/await 获取数据
  • 使用 unstable_cache 缓存频繁查询
  • 使用 revalidatePath / revalidateTag 精确控制缓存
  • Server Action 处理数据变更,配合 useActionState 管理表单状态

13.4 性能清单

  • ✅ 使用 next/image 优化图片
  • ✅ 使用 next/font 优化字体加载
  • ✅ 使用 Suspense 实现流式渲染
  • ✅ 使用 dynamic 懒加载非关键组件
  • ✅ 设置合理的 revalidate 缓存时间
  • ✅ 使用 generateStaticParams 预渲染热门页面
  • ✅ 配置 output: "standalone" 优化 Docker 部署体积

13.5 推荐技术栈

层级 推荐方案
UI 组件 shadcn/ui + Radix UI
样式 Tailwind CSS
表单 React Hook Form + Zod
状态管理 Zustand(轻量)/ Jotai
数据库 Prisma + PostgreSQL
认证 NextAuth.js / 自建 JWT
测试 Vitest + Testing Library
部署 Vercel / Docker + K8s

结语:Next.js 15 将 React 的服务端能力提升到了新高度。Server Components 减少了客户端 JavaScript,Server Actions 简化了数据变更,流式渲染提升了用户体验。掌握这些核心概念,你就能构建出高性能、SEO 友好的现代全栈应用。从一个简单的页面开始,逐步添加数据库、认证、API,你会发现全栈开发从未如此高效。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录