最后更新于:2026年7月

Next.js App Router 指南
Next.js 15:React 全栈框架的王者

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 RouterApp Router
路由pages/ 目录app/ 目录
布局_app.tsx 全局嵌套布局(每个路由可有独立布局)
数据获取getServerSideProps / getStaticProps直接 async/await
组件全部客户端默认服务端,按需客户端
APIpages/api/Server Actions / Route Handlers
加载状态手动管理内置 loading.tsx
错误处理_error.tsxerror.tsx 每个路由独立

建议:新项目一律使用 App Router。旧项目可渐进迁移。


二、项目初始化

2.1 创建项目

BASH
# 使用 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 项目结构

PLAINTEXT
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.json

2.3 核心配置

TS
// 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 文件系统路由

PLAINTEXT
文件路径                     → 路由 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 Handler

3.2 布局与模板

TSX
// 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>
  );
}
TSX
// 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 加载状态与错误处理

TSX
// 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>
  );
}
TSX
// 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 路由导航

TSX
// 使用 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(默认)

TSX
// 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

TSX
// 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 组合使用

TSX
// 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>
  );
}
TSX
// 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 选择原则

PLAINTEXT
默认使用 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 中获取数据

TSX
// 默认行为:请求时获取,缓存结果
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'每次请求都获取实时数据、用户信息
ISRnext: { revalidate: 3600 }定期重新验证新闻、产品列表

5.3 ISR(增量静态再生)

TSX
// 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 路由)

TS
// 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 });
}
TS
// 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 基本用法

TSX
// 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 在表单中使用

TSX
// 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)

TSX
// 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>
  );
}
TS
// 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(乐观更新)

TSX
// 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 基本配置

TS
// 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 高级中间件

TS
// 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 元数据配置

TSX
// 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 },
  },
};
TSX
// 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

TS
// 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,
  ];
}
TS
// 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 部署(推荐)

BASH
# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

# 生产部署
vercel --prod

9.2 Docker 部署

DOCKERFILE
# 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 输出:

TS
const nextConfig: NextConfig = {
  output: 'standalone',
};

9.3 Node.js 自托管

BASH
# 构建
pnpm build

# 启动
pnpm start

# 使用 PM2 管理
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup

十、性能优化

10.1 核心优化策略

PLAINTEXT
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 动态导入

TSX
import dynamic from 'next/dynamic';

// 懒加载重型组件
const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
  loading: () => <div>加载图表中...</div>,
  ssr: false,  // 不在服务端渲染
});

10.3 图片优化

TSX
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:

BASH
npm install next-auth@beta

Q3:App Router 项目能用 Pages Router 吗?

可以共存。 pages/app/ 目录可以同时存在,App Router 优先。


十二、总结

Next.js 学习路径

PLAINTEXT
入门(第1周):
  → 项目创建和基础路由
  → Server/Client Components 区别
  → 基础数据获取

进阶(第2-3周):
  → Server Actions
  → 缓存策略(SSR/SSG/ISR)
  → 中间件
  → SEO 优化

高级(第1-2月):
  → 并行路由、拦截路由
  → 流式渲染与 Suspense
  → 性能优化
  → 部署与运维

专家(第3月+):
  → 自定义 Server
  → Edge Runtime
  → Partial Prerendering
  → 企业级架构

【相关推荐】


Next.js 是 React 全栈开发的最佳选择。 掌握 App Router、Server Components 和 Server Actions,你就能构建高性能、SEO 友好的现代 Web 应用。🚀

版权声明

作者: 易邦

链接: https://blog.e8k.net/posts/nextjs15-app-router-guide-2026/

许可证: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。