Skip to content

Next.js 15 App Router 深度解析:从入门到实战 ​

发布时间:2026-03-27

Next.js 15 带来了 App Router 的全面成熟化,这是 React 服务端渲染的全新范式。本文从核心概念到实战用法,系统讲解 App Router 的设计思想与最佳实践。

Pages Router vs App Router ​

Next.js 14/15 存在两套路由系统并行:

特性Pages RouterApp Router
React 版本React 18React 18+ / React 19
服务端渲染页面级 SSR组件级 RSC
布局系统layout.tsx嵌套布局,共享状态
流式渲染不支持原生支持
数据获取getServerSidePropsasync/await 直接写
路由组不支持() 路由组
中间件独立文件独立文件

推荐新项目全部使用 App Router,Pages Router 会逐步被淘汰。


目录即路由 ​

App Router 的核心理念:目录结构 = 路由结构。

app/
├── layout.tsx          # 根布局(所有页面共享)
├── page.tsx             # 路由 /
├── page.module.css      # 页面样式
├── loading.tsx          # 加载状态(流式)
├── error.tsx           # 错误边界
├── not-found.tsx       # 404 页面
├── globals.css         # 全局样式
├── blog/
│   ├── page.tsx         # 路由 /blog
│   ├── loading.tsx      # /blog 的加载状态
│   └── [slug]/
│       └── page.tsx     # 路由 /blog/:slug(动态路由)
└── api/
    └── user/
        └── route.ts     # API 路由 /api/user

核心概念一:React Server Components ​

App Router 的革命性变化是默认所有组件都是 React Server Components(RSC)。

服务端组件 vs 客户端组件 ​

tsx
// app/page.tsx —— 默认是服务端组件
// 可以:直接访问数据库、文件系统、API 密钥
// 不能:使用 useState、useEffect、浏览器 API

import { db } from './lib/db'

export default async function HomePage() {
  // 直接 await 数据库查询,无需 API 路由
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  })

  return (
    <main>
      <h1>最新文章</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  )
}
tsx
// app/counter.tsx —— 需要交互?用 'use client'
'use client'

import { useState } from 'react'

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

  return (
    <button onClick={() => setCount(c => c + 1)}>
      点击次数:{count}
    </button>
  )
}
tsx
// app/page.tsx —— 服务端组件中嵌入客户端组件
import { Counter } from './counter'

export default async function HomePage() {
  const data = await fetchData() // 服务端获取数据

  return (
    <div>
      <h1>{data.title}</h1>
      {/* 客户端组件:可以响应用户交互 */}
      <Counter />
    </div>
  )
}

关键规则:

  • 服务端组件不能导入客户端组件(反过来可以)
  • 服务端组件的 props 必须可序列化
  • 敏感操作(数据库、密钥)只能在服务端组件中进行

核心概念二:嵌套布局 ​

布局系统是 App Router 最强大的特性之一:

tsx
// app/dashboard/layout.tsx
// /dashboard 及其所有子路由共享这个布局
export default function DashboardLayout({
  children,   // 子页面内容
  params,     // 动态路由参数
  searchParams, // URL 查询参数
}: {
  children: React.ReactNode
  params: { slug: string }
  searchParams: { [key: string]: string | string[] }
}) {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

布局之间状态共享 ​

tsx
// app/providers.tsx
'use client'

import { createContext } from 'react'

export const ThemeContext = createContext('light')

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  )
}
tsx
// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

核心概念三:数据获取 ​

App Router 最大的改进:在组件内直接使用 async/await,不需要 getServerSideProps 等特殊函数。

并行数据获取 ​

tsx
// app/user/page.tsx
export default async function UserPage() {
  // 两个请求并行发起,不需要手动 Promise.all
  const [user, posts] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
  ])

  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={posts} />
    </div>
  )
}

缓存策略 ​

tsx
// 默认缓存:每个请求重新获取
const data = await fetch('https://api.example.com/data')

// 重新验证:每 60 秒重新生成
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// 静态渲染:构建时生成,不重新验证不更新
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache' // 默认行为
})

// 动态数据:每个请求都重新获取
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})

使用 React Query / SWR ​

tsx
// app/posts/page.tsx
'use client'

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

export default function PostsPage() {
  const { data, error, isLoading } = useSWR('/api/posts', fetcher)

  if (isLoading) return <div>加载中...</div>
  if (error) return <div>加载失败</div>

  return (
    <div>
      {data.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )
}

核心概念四:动态路由 ​

静态路径生成 ​

tsx
// app/blog/[slug]/page.tsx

// 1. 定义动态路由参数类型
interface PageProps {
  params: { slug: string }
}

// 2. 生成静态路径(构建时)
export async function generateStaticParams() {
  const posts = await db.post.findMany()
  return posts.map(post => ({
    slug: post.slug,
  }))
}

// 3. 渲染页面
export default async function BlogPostPage({ params }: PageProps) {
  const post = await db.post.findUnique({
    where: { slug: params.slug },
  })

  if (!post) notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

Catch-all 路由 ​

app/docs/[...slug]/page.tsx

匹配 /docs/a、/docs/a/b、/docs/a/b/c 等所有路径:

tsx
export default function DocPage({
  params,
}: {
  params: { slug: string[] }
}) {
  const path = params.slug.join('/')
  return <div>当前路径:/{path}</div>
}

核心概念五:流式渲染与 Suspense ​

流式渲染让页面可以分块逐步显示,大幅优化用户体验:

tsx
// app/blog/page.tsx
import { Suspense } from 'react'

// 数据获取组件(可以是异步的)
async function BlogList() {
  const posts = await getPosts() // 可能需要 2 秒
  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  )
}

// 骨架屏组件
function BlogSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-6 bg-gray-200 rounded w-3/4 mb-4" />
      <div className="h-6 bg-gray-200 rounded w-1/2" />
    </div>
  )
}

export default function BlogPage() {
  return (
    <div>
      <h1>博客列表</h1>
      {/* Suspense 包裹异步组件,显示 fallback 直到数据加载完成 */}
      <Suspense fallback={<BlogSkeleton />}>
        <BlogList />
      </Suspense>
    </div>
  )
}

加载效果:页面骨架 → 数据加载完成 → 替换内容(无需等待整个页面)


核心概念六:中间件 ​

中间件在路由匹配前执行,适合认证、日志、重定向等:

typescript
// middleware.ts(放在项目根目录)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 获取用户 token
  const token = request.cookies.get('auth-token')

  // 未登录且访问受保护页面,重定向到登录页
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // 添加响应头
  const response = NextResponse.next()
  response.headers.set('x-custom-header', 'hello')
  return response
}

// 配置匹配路径
export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
}

实战:完整 CRUD 页面 ​

tsx
// app/posts/[id]/page.tsx
import { db } from '@/lib/db'
import { notFound } from 'next/navigation'
import { PostActions } from './post-actions' // 'use client' 组件

interface PageProps {
  params: { id: string }
}

export async function generateStaticParams() {
  const posts = await db.post.findMany()
  return posts.map(p => ({ id: p.id }))
}

export default async function PostPage({ params }: PageProps) {
  const post = await db.post.findUnique({
    where: { id: params.id },
    include: { author: true, comments: true },
  })

  if (!post) notFound()

  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <span>作者:{post.author.name}</span>
        <span>发布于:{post.createdAt.toLocaleDateString()}</span>
      </header>

      <div className="prose">{post.content}</div>

      {/* 客户端组件处理交互 */}
      <PostActions postId={post.id} />

      <section className="comments">
        <h2>评论({post.comments.length})</h2>
        {post.comments.map(c => (
          <div key={c.id}>
            <strong>{c.author.name}</strong>
            <p>{c.content}</p>
          </div>
        ))}
      </section>
    </article>
  )
}

性能优化技巧 ​

图片优化 ​

tsx
import Image from 'next/image'

export default function ProductImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      priority // LCP 图片加 priority 加速
      placeholder="blur"
      blurDataURL={blurData} // 低带宽友好
    />
  )
}

字体优化 ​

tsx
// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // FOIT 优化
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

动态导入 ​

tsx
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // 客户端渲染,不需要服务端
})

export default function DashboardPage() {
  return (
    <div>
      <HeavyChart data={chartData} />
    </div>
  )
}

常见问题 ​

Q:数据请求应该放在服务端还是客户端?

优先服务端组件。数据库查询、API 密钥、外部 API 调用都放服务端。只有需要交互(点击、表单、实时更新)时才用客户端组件。

Q:如何选择 generateStaticParams 还是每次请求都查?

有固定数据集(文章列表、商品列表)用 generateStaticParams 生成静态页面;数据频繁变化(实时价格、用户数据)用动态渲染。

Q:客户端组件可以调用数据库吗?

不可以。客户端组件在浏览器运行,数据库连接只能放在服务端组件或 API Route 中。


总结 ​

App Router 的核心优势:

优势说明
RSC 默认服务端组件零配置,数据获取极简
嵌套布局页面布局复用逻辑大幅简化
流式渲染Suspense 实现真正的渐进式加载
并行数据获取async/await 自然并行,无需手动 Promise.all
静态生成generateStaticParams 轻松实现 SSG
中间件认证、日志、重定向统一处理

App Router 代表了 React 全栈开发的未来方向,尽早掌握能大幅提升开发效率和用户体验。


本文由小虾子 撰写

Last updated: