前言

Next.js 15 带来了许多令人兴奋的新特性,特别是 App Router 的成熟和稳定。相比传统的 Pages Router,App Router 提供了更强大的路由控制、更好的性能优化和更直观的文件组织方式。

本文将带您从零开始,使用 Next.js 15 App Router 构建一个完整的现代化 Web 应用,深入探讨每个核心概念和实战技巧。

一、App Router 核心概念

1.1 App Router vs Pages Router

特性Pages Router (传统)App Router (新)
文件位置pages/ 目录app/ 目录
路由方式文件系统路由文件系统路由
数据获取getServerSideProps服务器组件 + fetch
布局需要 _app.js内置 Layout 系统
加载状态手动实现自动 loading.js
错误处理_error.jserror.js
流式渲染不支持支持
服务器组件不支持原生支持

1.2 文件组织结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
my-app/
├── app/
│   ├── (marketing)/           # 路由组 - 不影响URL
│   │   ├── about/
│   │   │   └── page.js        # /about
│   │   ├── layout.js          # 共享布局
│   │   └── page.js            # / (首页)
│   ├── (dashboard)/           # 路由组
│   │   ├── dashboard/
│   │   │   ├── layout.js      # Dashboard布局
│   │   │   ├── page.js        # /dashboard
│   │   │   └── settings/
│   │   │       └── page.js    # /dashboard/settings
│   │   └── layout.js          # 认证布局
│   ├── api/                   # API路由
│   │   └── users/
│   │       └── route.js       # /api/users
│   ├── layout.js              # 根布局
│   ├── page.js                # 根页面
│   ├── loading.js             # 全局加载状态
│   ├── error.js               # 全局错误处理
│   ├── not-found.js           # 404页面
│   └── globals.css            # 全局样式
├── public/                    # 静态资源
├── components/                # 共享组件
├── lib/                       # 工具函数
└── next.config.js             # Next.js配置

二、路由系统实战

2.1 动态路由

基础动态路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// app/blog/[slug]/page.tsx
interface BlogPostProps {
  params: {
    slug: string
  }
}

export default async function BlogPost({ params }: BlogPostProps) {
  const { slug } = params

  // 在服务器端获取数据
  const post = await fetch(`https://api.example.com/posts/${slug}`)
    .then(res => res.json())

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

捕获所有路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// app/docs/[...slug]/page.tsx
// 匹配 /docs/a, /docs/a/b, /docs/a/b/c 等

export default async function DocPage({ params }: {
  params: { slug: string[] }
}) {
  const path = params.slug.join('/')

  const content = await fetch(`https://api.example.com/docs/${path}`)
    .then(res => res.json())

  return (
    <div>
      <h1>文档: {path}</h1>
      <div>{content.body}</div>
    </div>
  )
}

2.2 路由组(Route Groups)

路由组允许您组织文件结构而不影响 URL 路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// app/(marketing)/about/page.tsx -> /about
// app/(marketing)/contact/page.tsx -> /contact
// app/(dashboard)/profile/page.tsx -> /profile

// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <MarketingHeader />
      <main>{children}</main>
      <MarketingFooter />
    </div>
  )
}

// app/(dashboard)/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

2.3 平行路由(Parallel Routes)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/dashboard/@dashboard/layout.tsx
export default function DashboardLayout({
  children,       // 主内容
  analytics,      // /dashboard/@analytics
  notifications,  // /dashboard/@notifications
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  notifications: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-[1fr_300px]">
      <div className="main">
        {children}
      </div>
      <aside className="sidebar">
        {analytics}
        {notifications}
      </aside>
    </div>
  )
}

三、服务器组件(Server Components)

3.1 理解服务器组件

服务器组件(RSC)在服务器上渲染,不发送 JavaScript 到客户端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// app/components/ProductList.tsx (服务器组件)
// ✅ 可以直接访问数据库、文件系统、后端API
// ❌ 不能使用 useState、useEffect、事件处理器

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

export default async function ProductList() {
  // 直接查询数据库
  const products = await db.product.findMany()

  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>{product.price}</p>
        </div>
      ))}
    </div>
  )
}

3.2 客户端组件

客户端组件需要 'use client' 指令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// app/components/ProductCard.tsx (客户端组件)
'use client'

import { useState } from 'react'

export function ProductCard({ product }: { product: Product }) {
  const [liked, setLiked] = useState(false)

  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </div>
  )
}

3.3 组合使用

最佳实践: 尽可能使用服务器组件,只在需要交互时使用客户端组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// app/products/page.tsx
import { ProductCard } from '@/components/ProductCard'
import { db } from '@/lib/db'

// 服务器组件
export default async function ProductsPage() {
  const products = await db.product.findMany()

  return (
    <div>
      <h1>产品列表</h1>
      <div className="grid">
        {products.map(product => (
          // 只在交互部分使用客户端组件
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  )
}

四、数据获取与缓存

4.1 Server Actions

Server Actions 允许从客户端直接调用服务器函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'

export async function createProduct(formData: FormData) {
  const name = formData.get('name') as string
  const price = parseFloat(formData.get('price') as string)

  const product = await db.product.create({
    data: { name, price }
  })

  // 重新验证缓存
  revalidatePath('/products')

  return product
}

export async function deleteProduct(id: string) {
  await db.product.delete({ where: { id } })
  revalidatePath('/products')
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// app/products/page.tsx
import { createProduct, deleteProduct } from '@/actions'

export default function ProductsPage() {
  return (
    <div>
      <form action={createProduct}>
        <input name="name" placeholder="产品名称" />
        <input name="price" type="number" placeholder="价格" />
        <button type="submit">添加</button>
      </form>

      <ProductList onDelete={deleteProduct} />
    </div>
  )
}

4.2 数据缓存策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// app/products/page.tsx
import { unstable_cache } from 'next/cache'

// 缓存数据获取
const getProducts = unstable_cache(
  async () => {
    return await db.product.findMany()
  },
  ['products'],  // 缓存键
  {
    revalidate: 3600,  // 1小时
    tags: ['products']  // 标签用于手动重新验证
  }
)

export default async function ProductsPage() {
  const products = await getProducts()

  return <ProductList products={products} />
}

4.3 流式渲染(Streaming)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function DashboardPage() {
  return (
    <div>
      <h1>仪表板</h1>

      {/* 独立加载每个部分 */}
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <Chart />
      </Suspense>

      <Suspense fallback={<RecentActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  )
}

五、布局系统

5.1 嵌套布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// app/layout.tsx (根布局)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN">
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  )
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard-layout">
      <DashboardNav />
      <main>{children}</main>
    </div>
  )
}

5.2 路由特定布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// app/(auth)/login/layout.tsx
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="max-w-md w-full">
        {children}
      </div>
    </div>
  )
}

5.3 模板(Templates)

模板在每次导航时重新渲染,不同于布局:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <div className="transition-opacity">
        {children}
      </div>
    </div>
  )
}

六、加载与错误状态

6.1 加载状态(Loading UI)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="space-y-4">
      {[...Array(6)].map((_, i) => (
        <div key={i} className="animate-pulse">
          <div className="h-4 bg-gray-200 rounded w-3/4"></div>
          <div className="h-4 bg-gray-200 rounded w-1/2 mt-2"></div>
        </div>
      ))}
    </div>
  )
}

6.2 错误处理(Error UI)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// app/products/error.tsx
'use client'  // 错误组件必须是客户端组件

export default function ProductsError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div className="error-container">
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  )
}

6.3 全局错误处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>发生严重错误!</h2>
        <button onClick={reset}>重试</button>
      </body>
    </html>
  )
}

七、元数据优化

7.1 静态元数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: '我的应用',
  description: '使用 Next.js 15 构建',
  keywords: ['Next.js', 'React', 'Web'],
  authors: [{ name: '作者名' }],
  openGraph: {
    title: '我的应用',
    description: '使用 Next.js 15 构建',
    images: ['/og-image.png'],
  },
}

7.2 动态元数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

interface Props {
  params: { slug: string }
}

// generateMetadata 函数
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json())

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPost({ params }: Props) {
  // ...
}

7.3 元数据模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: '默认标题',
    template: '%s | 我的应用'  // 自动应用
  },
  description: '默认描述',
  openGraph: {
    title: '默认OG标题',
    description: '默认OG描述',
    siteName: '我的应用',
  },
}

// 使用
export async function generateMetadata(): Promise<Metadata> {
  return {
    title: '关于我们',  // 自动变成 "关于我们 | 我的应用"
  }
}

八、中间件(Middleware)

8.1 身份验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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')?.value

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

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*']
}

8.2 国际化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// middleware.ts
export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // 检查路径是否已包含语言
  const pathnameIsMissingLocale = ['/', '/zh', '/en'].every(
    locale => !pathname.startsWith(locale)
  )

  // 如果没有语言前缀,重定向
  if (pathnameIsMissingLocale) {
    const locale = request.headers.get('accept-language')?.includes('zh')
      ? 'zh'
      : 'en'

    return NextResponse.redirect(
      new URL(`/${locale}${pathname}`, request.url)
    )
  }
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}

8.3 A/B 测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// middleware.ts
export function middleware(request: NextRequest) {
  // 获取或设置用户变体
  let variant = request.cookies.get('ab-test-variant')?.value

  if (!variant) {
    variant = Math.random() > 0.5 ? 'A' : 'B'
    const response = NextResponse.rewrite(request.nextUrl)
    response.cookies.set('ab-test-variant', variant)
    return response
  }

  // 重写到变体页面
  if (variant === 'B') {
    const url = request.nextUrl.clone()
    url.pathname = `/b${url.pathname}`
    return NextResponse.rewrite(url)
  }
}

九、实战案例:博客系统

9.1 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
blog-app/
├── app/
│   ├── (main)/
│   │   ├── page.tsx           # 首页
│   │   ├── blog/
│   │   │   ├── page.tsx       # 博客列表
│   │   │   ├── [slug]/
│   │   │   │   ├── page.tsx   # 博客详情
│   │   │   │   └── edit/
│   │   │   │       └── page.tsx  # 编辑页面
│   │   │   └── new/
│   │   │       └── page.tsx   # 新建博客
│   │   ├── layout.tsx         # 主布局
│   │   └── loading.tsx
│   ├── (auth)/
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── register/
│   │   │   └── page.tsx
│   │   └── layout.tsx
│   ├── api/
│   │   └── blog/
│   │       └── route.ts
│   ├── layout.tsx
│   └── globals.css
├── components/
│   ├── BlogCard.tsx
│   ├── MarkdownRenderer.tsx
│   └── CommentSection.tsx
├── lib/
│   ├── db.ts
│   ├── auth.ts
│   └── utils.ts
└── types/
    └── blog.ts

9.2 博客列表页

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// app/(main)/blog/page.tsx
import Link from 'next/link'
import { db } from '@/lib/db'

export const revalidate = 3600  // ISR: 每小时重新验证

export default async function BlogPage({
  searchParams,
}: {
  searchParams: { page?: string }
}) {
  const page = parseInt(searchParams.page || '1')
  const limit = 10
  const skip = (page - 1) * limit

  const [posts, total] = await Promise.all([
    db.post.findMany({
      skip,
      take: limit,
      orderBy: { createdAt: 'desc' },
      include: {
        author: { select: { name: true, avatar: true } }
      }
    }),
    db.post.count()
  ])

  const totalPages = Math.ceil(total / limit)

  return (
    <div>
      <h1>博客</h1>

      <div className="grid gap-6">
        {posts.map(post => (
          <Link
            key={post.id}
            href={`/blog/${post.slug}`}
            className="block p-6 border rounded-lg hover:shadow-lg transition"
          >
            <h2>{post.title}</h2>
            <p className="text-gray-600 mt-2">{post.excerpt}</p>
            <div className="flex items-center gap-2 mt-4">
              <img
                src={post.author.avatar}
                alt={post.author.name}
                className="w-8 h-8 rounded-full"
              />
              <span>{post.author.name}</span>
              <span>·</span>
              <time>
                {new Date(post.createdAt).toLocaleDateString('zh-CN')}
              </time>
            </div>
          </Link>
        ))}
      </div>

      <Pagination
        currentPage={page}
        totalPages={totalPages}
        baseUrl="/blog"
      />
    </div>
  )
}

9.3 博客详情页

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// app/(main)/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { db } from '@/lib/db'
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
import { CommentSection } from '@/components/CommentSection'

export async function generateMetadata({ params }: {
  params: { slug: string }
}) {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  })

  if (!post) return {}

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage || '/default-og.png'],
      type: 'article',
      publishedTime: post.createdAt.toISOString(),
      authors: [post.author.name]
    }
  }
}

export default async function BlogPostPage({ params }: {
  params: { slug: string }
}) {
  const post = await db.post.findUnique({
    where: { slug: params.slug },
    include: {
      author: { select: { name: true, avatar: true, bio: true } },
      comments: {
        include: { author: { select: { name: true, avatar: true } } }
      }
    }
  })

  if (!post) {
    notFound()
  }

  return (
    <article className="max-w-4xl mx-auto">
      <header className="mb-8">
        <h1 className="text-4xl font-bold">{post.title}</h1>
        <div className="flex items-center gap-4 mt-4">
          <img
            src={post.author.avatar}
            alt={post.author.name}
            className="w-12 h-12 rounded-full"
          />
          <div>
            <div className="font-medium">{post.author.name}</div>
            <div className="text-sm text-gray-600">
              {new Date(post.createdAt).toLocaleDateString('zh-CN')}
            </div>
          </div>
        </div>
      </header>

      <div className="prose prose-lg max-w-none">
        <MarkdownRenderer content={post.content} />
      </div>

      <footer className="mt-12 pt-8 border-t">
        <div className="flex gap-4">
          {post.tags.map(tag => (
            <span
              key={tag}
              className="px-3 py-1 bg-gray-100 rounded-full text-sm"
            >
              {tag}
            </span>
          ))}
        </div>
      </footer>

      <CommentSection postId={post.id} comments={post.comments} />
    </article>
  )
}

9.4 API 路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// app/api/blog/route.ts
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { db } from '@/lib/db'
import { postSchema } from '@/lib/validations'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = parseInt(searchParams.get('page') || '1')
  const limit = parseInt(searchParams.get('limit') || '10')

  const posts = await db.post.findMany({
    skip: (page - 1) * limit,
    take: limit,
    orderBy: { createdAt: 'desc' }
  })

  return NextResponse.json(posts)
}

export async function POST(request: Request) {
  const session = await getServerSession(authOptions)

  if (!session) {
    return NextResponse.json(
      { error: '未授权' },
      { status: 401 }
    )
  }

  const body = await request.json()
  const validatedData = postSchema.parse(body)

  const post = await db.post.create({
    data: {
      ...validatedData,
      authorId: session.user.id
    }
  })

  return NextResponse.json(post, { status: 201 })
}

十、性能优化

10.1 图片优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import Image from 'next/image'

export default function ProductPage() {
  return (
    <Image
      src="/product.jpg"
      alt="产品图片"
      width={800}
      height={600}
      priority  // 首屏图片优先加载
      placeholder="blur"  // 模糊占位符
      blurDataURL="data:image/jpeg;base64,..."  // 模糊数据
    />
  )
}

10.2 字体优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter'
})

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

10.3 动态导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// app/dashboard/page.tsx
import dynamic from 'next/dynamic'

// 动态导入重型组件
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false  // 仅客户端渲染
})

const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), {
  loading: () => <div>加载中...</div>
})

export default function DashboardPage() {
  return (
    <div>
      <Chart />
      <RichTextEditor />
    </div>
  )
}

结语

Next.js 15 的 App Router 为构建现代化 Web 应用提供了强大而优雅的解决方案。通过本文的实战指南,您应该能够:

  1. 理解核心概念:掌握 App Router 的路由系统和文件组织
  2. 服务器组件:合理使用 RSC 和客户端组件
  3. 数据管理:实现高效的数据获取和缓存策略
  4. 用户体验:提供流畅的加载和错误处理
  5. 性能优化:应用各种优化技巧提升应用性能

下一步建议:

开始构建您的 Next.js 应用吧!


延伸阅读: