最后更新于:2026年7月

Next.js 是 React 生态的绝对主流框架。 根据 2025 年 State of JS 调查,Next.js 的使用率在 React 框架中排名第一,超过 50% 的 React 开发者使用 Next.js。从 Vercel 官网到 Notion、TikTok 网页版,Next.js 在全球服务着数亿用户。
Next.js 15 基于 React 19,引入了 App Router、Server Components、Server Actions 等革命性特性,彻底改变了 React 应用的开发方式。
本文从核心架构到生产级实战,带你全面掌握 Next.js 15。
一、Next.js 15 是什么?
1.1 核心特性
| 特性 | 说明 |
|---|---|
| App Router | 基于文件系统的路由,支持布局、加载状态、错误处理 |
| Server Components | 默认在服务端渲染,减少客户端 JS 体积 |
| Server Actions | 服务端函数,替代传统 API 路由 |
| 流式渲染 | 逐步传输 HTML,提升首屏加载速度 |
| Turbopack | 新一代打包工具,开发体验更快 |
| 数据缓存 | 智能请求去重与缓存 |
1.2 App Router vs Pages Router
| 特性 | Pages Router | App Router |
|---|---|---|
| 路由 | pages/ 目录 | app/ 目录 |
| 布局 | _app.tsx 全局 | 嵌套布局(每个路由可有独立布局) |
| 数据获取 | getServerSideProps / getStaticProps | 直接 async/await |
| 组件 | 全部客户端 | 默认服务端,按需客户端 |
| API | pages/api/ | Server Actions / Route Handlers |
| 加载状态 | 手动管理 | 内置 loading.tsx |
| 错误处理 | _error.tsx | error.tsx 每个路由独立 |
建议:新项目一律使用 App Router。旧项目可渐进迁移。
二、项目初始化
2.1 创建项目
# 使用 create-next-app
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? … Yes
✔ Would you like to customize the import alias? … @/*
# 一步到位
npx create-next-app@latest my-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--turbopack \
--import-alias "@/*"2.2 项目结构
my-app/
├── src/
│ ├── app/ # App Router 路由目录
│ │ ├── layout.tsx # 根布局(必须)
│ │ ├── page.tsx # 首页
│ │ ├── loading.tsx # 全局加载状态
│ │ ├── error.tsx # 全局错误处理
│ │ ├── not-found.tsx # 404 页面
│ │ ├── globals.css # 全局样式
│ │ │
│ │ ├── about/
│ │ │ └── page.tsx # /about 路由
│ │ │
│ │ ├── blog/
│ │ │ ├── page.tsx # /blog 路由
│ │ │ ├── [slug]/
│ │ │ │ └── page.tsx # /blog/xxx 动态路由
│ │ │ └── loading.tsx # blog 加载状态
│ │ │
│ │ ├── dashboard/
│ │ │ ├── layout.tsx # dashboard 独立布局
│ │ │ ├── page.tsx
│ │ │ └── settings/
│ │ │ └── page.tsx
│ │ │
│ │ └── api/ # Route Handlers
│ │ └── hello/
│ │ └── route.ts
│ │
│ ├── components/ # 组件目录
│ │ ├── ui/ # UI 基础组件
│ │ └── features/ # 功能组件
│ │
│ ├── lib/ # 工具函数
│ │ ├── utils.ts
│ │ └── db.ts
│ │
│ └── types/ # 类型定义
│ └── index.ts
│
├── public/ # 静态资源
├── next.config.ts # Next.js 配置
├── tailwind.config.ts # Tailwind 配置
├── tsconfig.json # TypeScript 配置
└── package.json2.3 核心配置
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// 图片优化
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com' },
],
},
// 重定向
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true,
},
];
},
// 重写
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:3001/:path*',
},
];
},
// 自定义 Header
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
];
},
};
export default nextConfig;三、路由系统
3.1 文件系统路由
文件路径 → 路由 URL
app/page.tsx → /
app/about/page.tsx → /about
app/blog/page.tsx → /blog
app/blog/[slug]/page.tsx → /blog/hello-world
app/shop/[...slug]/page.tsx → /shop/a/b/c (Catch-all)
app/shop/[[...slug]]/page.tsx → /shop, /shop/a, /shop/a/b (可选 Catch-all)
特殊文件:
layout.tsx → 布局(路由切换时保持不变)
page.tsx → 页面(路由的唯一 UI)
loading.tsx → 加载状态(React Suspense)
error.tsx → 错误处理(错误边界)
not-found.tsx → 404 页面
template.tsx → 模板(类似 layout 但每次路由切换都重新挂载)
default.tsx → 并行路由的默认页面
route.ts → API Route Handler3.2 布局与模板
// app/layout.tsx — 根布局(必须包含 html 和 body)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<nav>
<a href="/">首页</a>
<a href="/blog">博客</a>
<a href="/about">关于</a>
</nav>
<main>{children}</main>
<footer>© 2026 My App</footer>
</body>
</html>
);
}// app/dashboard/layout.tsx — dashboard 独立布局
import Sidebar from '@/components/sidebar';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar />
<div className="flex-1">{children}</div>
</div>
);
}3.3 加载状态与错误处理
// app/blog/loading.tsx — 加载状态
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-4" />
<div className="h-4 bg-gray-200 rounded w-2/3 mb-2" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
);
}// app/blog/error.tsx — 错误处理(必须是客户端组件)
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}3.4 路由导航
// 使用 Link 组件(客户端导航,不刷新页面)
import Link from 'next/link';
export default function Nav() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/blog">博客</Link>
<Link href={`/blog/${slug}`}>文章</Link>
</nav>
);
}
// 编程式导航
'use client';
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const router = useRouter();
const handleSubmit = async () => {
// ... 登录逻辑
router.push('/dashboard');
router.refresh(); // 刷新当前路由(重新获取数据)
};
return <button onClick={handleSubmit}>登录</button>;
}四、Server Components 与 Client Components
4.1 Server Components(默认)
// app/blog/page.tsx — 默认是 Server Component
import { db } from '@/lib/db';
// ✅ 可以直接访问数据库
// ✅ 可以使用 Node.js API
// ✅ 不会发送 JS 到客户端
// ❌ 不能使用 useState, useEffect, onClick
// ❌ 不能使用浏览器 API
interface Post {
id: number;
title: string;
content: string;
}
export default async function BlogPage() {
const posts: Post[] = await db.post.findMany();
return (
<div>
<h1>博客文章</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}4.2 Client Components
// components/counter.tsx — 使用 'use client' 声明
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}4.3 组合使用
// app/page.tsx — Server Component 中使用 Client Component
import { db } from '@/lib/db';
import LikeButton from '@/components/like-button'; // Client Component
export default async function Page() {
const post = await db.post.findUnique({ where: { id: 1 } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Server Component 传数据给 Client Component */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
);
}// components/like-button.tsx — Client Component
'use client';
import { useState } from 'react';
interface LikeButtonProps {
postId: number;
initialLikes: number;
}
export default function LikeButton({ postId, initialLikes }: LikeButtonProps) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, setIsPending] = useState(false);
const handleLike = async () => {
setIsPending(true);
// 调用 Server Action
const result = await likePost(postId);
setLikes(result.likes);
setIsPending(false);
};
return (
<button onClick={handleLike} disabled={isPending}>
❤️ {likes}
</button>
);
}4.4 选择原则
默认使用 Server Component,只在需要时使用 Client Component:
需要 Client Component 的场景:
✅ 使用 useState, useReducer, useEffect
✅ 使用 onClick, onChange 等事件处理
✅ 使用浏览器 API(window, localStorage)
✅ 使用仅客户端的库(如地图、图表)
✅ 使用 React Context
保持 Server Component 的场景:
✅ 获取数据(数据库、API)
✅ 访问后端资源
✅ 保密信息(API Key、Token)
✅ 大型依赖(减少客户端 JS 体积)五、数据获取与缓存
5.1 Server Component 中获取数据
// 默认行为:请求时获取,缓存结果
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
// 缓存策略
async function getPostsCached() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache', // 缓存(默认行为)
// cache: 'no-store', // 不缓存(每次都获取)
next: { revalidate: 3600 }, // 重新验证间隔(秒)
});
return res.json();
}5.2 缓存策略对比
| 策略 | 配置 | 说明 | 适用场景 |
|---|---|---|---|
| 静态 | cache: 'force-cache' | 构建时获取,永久缓存 | 博客文章、文档 |
| 动态 | cache: 'no-store' | 每次请求都获取 | 实时数据、用户信息 |
| ISR | next: { revalidate: 3600 } | 定期重新验证 | 新闻、产品列表 |
5.3 ISR(增量静态再生)
// app/blog/[slug]/page.tsx
interface Post {
title: string;
content: string;
updatedAt: string;
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 600 }, // 每 10 分钟重新验证
});
return res.json();
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<time>{post.updatedAt}</time>
</article>
);
}
// 生成静态路径
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}5.4 Route Handlers(API 路由)
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
// GET 请求
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const posts = await fetchPosts(parseInt(page));
return NextResponse.json(posts);
}
// POST 请求
export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
}// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await deletePost(id);
return NextResponse.json({ success: true });
}六、Server Actions
6.1 基本用法
// app/actions.ts — Server Actions 文件
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
// 创建文章
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({ data: { title, content } });
// 重新验证缓存
revalidatePath('/blog');
// 重定向
redirect('/blog');
}
// 删除文章
export async function deletePost(id: number) {
await db.post.delete({ where: { id } });
revalidatePath('/blog');
}6.2 在表单中使用
// app/blog/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input
type="text"
name="title"
placeholder="标题"
required
/>
<textarea
name="content"
placeholder="内容"
required
/>
<button type="submit">发布</button>
</form>
);
}6.3 useActionState(带状态的 Action)
// components/post-form.tsx
'use client';
import { useActionState } from 'react';
import { createPostWithState } from '@/app/actions';
const initialState = { message: '' };
export default function PostForm() {
const [state, formAction, isPending] = useActionState(
createPostWithState,
initialState
);
return (
<form action={formAction}>
<input type="text" name="title" required />
<textarea name="content" required />
<button type="submit" disabled={isPending}>
{isPending ? '发布中...' : '发布'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}// app/actions.ts
'use server';
export async function createPostWithState(prevState: { message: string }, formData: FormData) {
try {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
if (!title || title.length < 2) {
return { message: '标题至少2个字符' };
}
await db.post.create({ data: { title, content } });
revalidatePath('/blog');
return { message: '发布成功!' };
} catch (error) {
return { message: '发布失败,请重试' };
}
}6.4 useOptimistic(乐观更新)
// components/like-button.tsx
'use client';
import { useOptimistic } from 'react';
import { likePost } from '@/app/actions';
interface LikeButtonProps {
postId: number;
likes: number;
}
export default function LikeButton({ postId, likes }: LikeButtonProps) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(currentLikes) => currentLikes + 1
);
const handleLike = async () => {
addOptimisticLike(undefined); // 立即乐观更新
await likePost(postId); // 服务端执行
};
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}七、中间件(Middleware)
7.1 基本配置
// middleware.ts — 项目根目录
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 获取 Cookie
const token = request.cookies.get('token')?.value;
// 检查登录状态
if (!token) {
// 未登录,重定向到登录页
return NextResponse.redirect(new URL('/login', request.url));
}
// 已登录,继续
return NextResponse.next();
}
// 配置匹配路径
export const config = {
matcher: [
'/dashboard/:path*',
'/settings/:path*',
'/api/protected/:path*',
],
};7.2 高级中间件
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. 认证检查
const token = request.cookies.get('token')?.value;
const protectedPaths = ['/dashboard', '/settings'];
if (protectedPaths.some((p) => pathname.startsWith(p)) && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// 2. 设置请求头
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-pathname', pathname);
// 3. A/B 测试
const bucket = request.cookies.get('bucket')?.value || 'a';
requestHeaders.set('x-bucket', bucket);
// 4. 地理位置路由
const country = request.geo?.country || 'US';
if (pathname === '/' && country === 'CN') {
return NextResponse.redirect(new URL('/zh', request.url));
}
return NextResponse.next({
request: { headers: requestHeaders },
});
}八、SEO 优化
8.1 元数据配置
// app/layout.tsx — 全局元数据
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App', // 子页面标题格式
},
description: 'My awesome application',
keywords: ['Next.js', 'React', 'TypeScript'],
authors: [{ name: 'Your Name' }],
creator: 'Your Name',
openGraph: {
type: 'website',
locale: 'zh_CN',
url: 'https://example.com',
siteName: 'My App',
title: 'My App',
description: 'My awesome application',
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'My awesome application',
},
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true },
},
};// app/blog/[slug]/page.tsx — 动态元数据
import type { Metadata, ResolvingMetadata } from 'next';
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}8.2 Sitemap 与 Robots
// app/sitemap.ts
import type { MetadataRoute } from 'next/server';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
const blogEntries = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [
{ url: 'https://example.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
{ url: 'https://example.com/blog', lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
...blogEntries,
];
}// app/robots.ts
import type { MetadataRoute } from 'next/server';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api/', '/dashboard/'],
},
sitemap: 'https://example.com/sitemap.xml',
};
}九、部署
9.1 Vercel 部署(推荐)
# 安装 Vercel CLI
npm i -g vercel
# 部署
vercel
# 生产部署
vercel --prod9.2 Docker 部署
# Dockerfile
FROM node:20-alpine AS base
# 依赖阶段
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
# 运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
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
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]next.config.ts 中需要启用 standalone 输出:
const nextConfig: NextConfig = {
output: 'standalone',
};9.3 Node.js 自托管
# 构建
pnpm build
# 启动
pnpm start
# 使用 PM2 管理
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup十、性能优化
10.1 核心优化策略
1. 减少客户端 JS
→ 默认使用 Server Components
→ 按需添加 'use client'
→ 使用 dynamic import 懒加载客户端组件
2. 数据获取优化
→ 并行获取数据(Promise.all)
→ 合理使用缓存策略
→ 使用 Suspense 流式渲染
3. 图片优化
→ 使用 next/image 组件
→ 自动格式转换(WebP/AVIF)
→ 按需加载
4. 字体优化
→ 使用 next/font
→ 自动内联字体 CSS
→ 消除布局偏移
5. 第三方脚本
→ 使用 next/script
→ 延迟加载非关键脚本10.2 动态导入
import dynamic from 'next/dynamic';
// 懒加载重型组件
const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
loading: () => <div>加载图表中...</div>,
ssr: false, // 不在服务端渲染
});10.3 图片优化
import Image from 'next/image';
export default function Avatar() {
return (
<Image
src="/avatar.jpg"
alt="用户头像"
width={64}
height={64}
priority // 优先加载(首屏图片)
placeholder="blur" // 模糊占位
quality={80}
/>
);
}十一、常见问题
Q1:Server Component 中能用 useEffect 吗?
不能。 Server Component 不支持任何 Hook(useState、useEffect、useCallback 等)。需要交互的组件必须标记 'use client'。
Q2:如何处理认证?
推荐使用 NextAuth.js(Auth.js)或 Clerk:
npm install next-auth@betaQ3:App Router 项目能用 Pages Router 吗?
可以共存。 pages/ 和 app/ 目录可以同时存在,App Router 优先。
十二、总结
Next.js 学习路径
入门(第1周):
→ 项目创建和基础路由
→ Server/Client Components 区别
→ 基础数据获取
进阶(第2-3周):
→ Server Actions
→ 缓存策略(SSR/SSG/ISR)
→ 中间件
→ SEO 优化
高级(第1-2月):
→ 并行路由、拦截路由
→ 流式渲染与 Suspense
→ 性能优化
→ 部署与运维
专家(第3月+):
→ 自定义 Server
→ Edge Runtime
→ Partial Prerendering
→ 企业级架构【相关推荐】
- React 19 + TypeScript 完全指南 - React 基础
- Vite 5 项目构建与性能优化 - 构建工具
- 前端测试完全指南 - 自动化测试
Next.js 是 React 全栈开发的最佳选择。 掌握 App Router、Server Components 和 Server Actions,你就能构建高性能、SEO 友好的现代 Web 应用。🚀
