Next.js 15 全栈开发完全教程
关键词:Next.js, React, 全栈开发, SSR, 前端框架
本教程全面讲解 Next.js 15 的核心特性与实战开发,从 App Router 架构到生产部署,带你构建高性能的现代 Web 应用。
目录
- Next.js 15 新特性概览
- 项目初始化与目录结构
- App Router 架构详解
- React Server Components
- Server Actions 与数据变更
- 流式渲染与 Suspense
- 中间件与认证
- API Routes 路由处理
- 数据库集成(Prisma)
- 部署优化
- SEO 优化
- 性能调优
- 总结与最佳实践
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,你会发现全栈开发从未如此高效。