引言:为什么 App Router 是 Next.js 最重要的架构变革

2026 年,Next.js App Router 已经从"新特性"变成了 React 全栈开发的实际标准。从 Pages Router 到 App Router 的迁移,不仅仅是文件目录的重新组织——它代表了从"页面为中心"到"组件树为中心"的思维范式的根本转变。App Router 引入了 React Server Components 作为默认渲染模式、文件系统级嵌套布局、Server Actions 直连后端、Streaming SSR 流式传输等一系列能力,让开发者可以用更少的代码构建出性能更强、体验更好的 Web 应用。

这篇文章将从实战角度深入讲解 App Router 的核心机制:Server Components 的运行原理与边界设计、嵌套 Layout 体系与路由分组策略、数据获取的最佳模式与缓存控制、Streaming SSR 的 Suspense 实现、以及 Server Actions 替代 API Routes 的全栈开发方式。每一节都配有可直接运行的代码示例,帮助你在下一个项目中无缝落地这些技术。

第一章:App Router 基础架构——从 Pages 到 App 的思维跃迁

1.1 文件系统路由的本质差异

Pages Router 的路由是基于文件的——pages/about.tsx 自动映射到 /about 路径。App Router 看似相同,但引入了革命性的概念:每个路由段都可以拥有自己的 UI、数据获取逻辑和加载状态。这意味着路由不再是简单的页面映射,而是可以嵌套的组件树节点。


# Pages Router 的目录结构
pages/
├── _app.tsx          # 全局布局(所有页面共享)
├── index.tsx         # /
├── about.tsx         # /about
└── blog/
    └── [slug].tsx    # /blog/:slug

# App Router 的目录结构
app/
├── layout.tsx        # 根布局(全站共享)
├── page.tsx          # /
├── loading.tsx       # 全局加载骨架
├── error.tsx         # 全局错误边界
├── not-found.tsx     # 全站 404
├── about/
│   ├── layout.tsx    # /about 段布局
│   └── page.tsx      # /about
└── blog/
    ├── layout.tsx    # /blog 段布局
    ├── [slug]/
    │   ├── page.tsx  # /blog/:slug
    │   └── loading.tsx  # 文章加载骨架
    └── page.tsx      # /blog(列表页)

最显著的区别:App Router 的 layout.tsx 不会在路由跳转时重新渲染——子路由切换时,共享的 Layout 保持状态不变。这在 Pages Router 中是做不到的(_app.tsx 每次页面切换都会 remount)。

1.2 路由分组与路径前缀分离

App Router 引入了用括号命名的文件夹作为"路由分组"——它们不会出现在 URL 路径中,只用于组织代码结构:


app/
├── (marketing)/        # 营销组 → 不体现在 URL
│   ├── layout.tsx      # 营销类页面的专属布局(带头部 CTA 按钮)
│   ├── page.tsx        # /
│   ├── pricing/
│   │   └── page.tsx    # /pricing
│   └── features/
│       └── page.tsx    # /features
├── (dashboard)/        # 管理组 → 不体现在 URL
│   ├── layout.tsx      # 管理后台布局(侧边栏导航)
│   ├── dashboard/
│   │   └── page.tsx    # /dashboard
│   └── settings/
│       └── page.tsx    # /settings
└── api/                # API 路由
    └── route.ts

这个机制让你可以为不同业务域设置完全不同的 Layout,而 URL 层级保持简洁。比如营销页面组用一个带 CTA 按钮的布局,管理后台用带侧边栏的布局——路由分组让这种组织方式变得自然。

1.3 默认 Server Component——零 JS 发送到客户端

App Router 中所有组件默认都是 Server Component——它们在服务端执行、渲染为 HTML 和流式数据,组件代码本身不会被打包到客户端 JS。这是性能提升的关键所在。


// app/posts/page.tsx —— 默认是 Server Component
// 这段代码永远不会出现在客户端 JS bundle 中
import { db } from '@/lib/database';

// 直接在组件中访问数据库层 —— 无需 API 层中转
async function getPosts() {
  const posts = await db.query(`
    SELECT id, title, excerpt, created_at, author_name 
    FROM posts 
    WHERE status = 'published' 
    ORDER BY created_at DESC 
    LIMIT 20
  `);
  return posts;
}

// Server Component 支持 async/await —— 直接做数据获取
export default async function PostsPage() {
  const posts = await getPosts();  // 服务端执行,客户端不感知

  return (
    

最新文章

{posts.map(post => (

{post.title}

{post.excerpt}

{post.author_name}
))}
); }

上面的代码中:数据库查询逻辑完全运行在服务端,客户端只接收最终的 HTML。整个组件的 JavaScript 代码(包括数据库驱动)都不会出现在浏览器中。

第二章:Server Components 深度剖析

2.1 RSC 的本质——一种新的渲染协议

React Server Components 不是 SSR,而是一种全新的渲染协议。理解这一点非常关键:

  • 传统 SSR:在服务端把组件渲染成 HTML 发送给浏览器,浏览器下载 JS 后需要"水合"整个组件树。客户端 JS bundle 中包含所有组件代码。
  • Server Components:组件在服务端执行,输出的是渲染后的 React 组件树描述(RSC Payload),一种类似虚拟 DOM 的序列化格式。这个 payload 不包含组件源代码,也不包含任何事件 handler 的代码。

这意味着 Server Component 包可以在服务端直接执行异步 I/O、访问文件系统、读取环境变量——这些能力在客户端是不存在的。同时由于组件代码不会被序列化到 payload 中,你可以安全地在 Server Component 中使用 API Key、数据库连接字符串等敏感信息。

2.2 use client 的边界设计——把交互孤岛化

"use client"不是一个普通的指令,它在编译阶段会为组件创建一条客户端边界。边界以内的所有代码(包括 import 的模块)都会被打包到客户端 JS bundle 中。

最重要的设计原则:尽可能把 'use client' 推到组件树的叶子节点。反面案例——在页面根部标记 use client,会导致页面内所有组件(包括纯展示型的)都被迫加载客户端 JS:


// 反模式:整个页面被标记为客户端组件
'use client'; // ⚠️ 整棵组件树都会进入客户端 JS

import { useState } from 'react';
import { ArticleList } from './ArticleList';       // 被迫变客户端 JS
import { ArticleCard } from './ArticleCard';       // 被迫变客户端 JS
import { MarkdownRenderer } from './MarkdownRenderer'; // 被迫变客户端 JS

export default function BlogPage() {
  const [filter, setFilter] = useState('all');
  return (
    <>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      
    &>
  );
}

正确的做法:保持父页面为 Server Component,只把真正需要交互的"叶子组件"标记为 Client Component:


// 正确模式:文章列表页面(Server Component)
import { db } from '@/lib/database';
import { ArticleList } from './ArticleList';       // 0 KB 客户端 JS
import { ArticleCard } from './ArticleCard';       // 0 KB 客户端 JS
import { FilterBar } from './FilterBar';           // ~2 KB 客户端 JS

export default async function BlogPage() {
  const articles = await db.query('SELECT * FROM articles');

  return (
    <>
                {/* 唯一需要客户端 JS 的组件 */}
      
    &>
  );
}

2.3 Server Component 与 Client Component 的数据传递规则

从 Server Component 向 Client Component 传递 props 时,数据必须是可序列化的(通过 RSC Payload 的序列化协议)。这意味着函数、类实例、Symbol 等无法通过边界传递:


// Server Component
import { LikeButton } from './LikeButton'; // Client Component

export default async function PostPage({ params }) {
  const post = await getPost(params.id);
  const user = await getCurrentUser();

  return (
    

{post.title}

{post.content}

{/* 传递一个标识符而不是函数或对象 */}
); } // Client Component 'use client'; export function LikeButton({ postId, userId }) { // 通过 Server Action 来执行"点赞"操作 return <button onClick={() => likePost(postId, userId)}>👍</button> }

第三章:数据获取与缓存——App Router 的核心优势

3.1 fetch 的自动去重与持久缓存

App Router 扩展了 Web 标准 fetch API,让同一个请求在组件树中自动去重——多个组件请求同一 URL 时,只会发出一次网络请求。更重要的是,这个缓存默认是持久化的:在构建时预取的数据会被写入硬盘缓存,后续请求直接命中。


// app/products/[id]/page.tsx
// 这个 fetch 会在服务端执行,默认缓存到 CDN/硬盘
async function getProduct(id: string) {
  const res = await fetch(`https://api.store.com/products/${id}`, {
    next: {
      revalidate: 3600,        // 1 小时后自动重新验证
      tags: ['product', `product-${id}`]  // 按需刷新标记
    }
  });
  return res.json();
}

// 同一个页面不同组件里调用了 getProduct('123')
// App Router 保证只会发出一个网络请求 —— 自动去重
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);  // 发出请求
  return (
    <>
              // 重用缓存,不重复请求
       // 不重复请求
       // 不重复请求
    &>
  );
}

3.2 五种缓存策略的精确选择

App Router 提供五种不同的缓存策略,对应不同的业务场景:


// 策略 1: 静态生成 (SSG) —— 构建时永久缓存
fetch(url, { cache: 'force-cache' });

// 策略 2: 动态渲染 —— 每次请求都重新获取
fetch(url, { cache: 'no-store' });

// 策略 3: ISR 增量静态再生 —— 按时间间隔自动刷新
fetch(url, { next: { revalidate: 60 } });

// 策略 4: 按需重新验证 —— 通过 tag 或 path 主动清除缓存
fetch(url, { next: { tags: ['collection'] } });
// 在 Server Action 中: revalidateTag('collection');

// 策略 5: 请求级记忆化 —— 单次渲染内相同 URL 去重
// (Package-level memoization,自动生效,无需配置)

3.3 数据库直连——消灭不必要的 API 层

Server Component 可以直接操作数据库,省去传统 BFF 层(Backend for Frontend):


// app/dashboard/page.tsx
import { db } from '@/lib/db';
import { currentUser } from '@clerk/nextjs/server';
import { StatsCard } from './StatsCard';
import { RecentOrders } from './RecentOrders';

export default async function DashboardPage() {
  const user = await currentUser();

  // 并行数据获取 —— 三个独立的数据库查询同时执行
  const [stats, orders, notifications] = await Promise.all([
    db.selectFrom('orders')
      .select(['status', db.fn.count('id').as('count')])
      .where('user_id', '=', user.id)
      .groupBy('status')
      .execute(),
    db.selectFrom('orders')
      .selectAll()
      .where('user_id', '=', user.id)
      .orderBy('created_at', 'desc')
      .limit(10)
      .execute(),
    db.selectFrom('notifications')
      .selectAll()
      .where('user_id', '=', user.id)
      .where('read', '=', false)
      .execute()
  ]);

  return (
    
); }

第四章:Streaming SSR 与 Suspense 细粒度流式渲染

4.1 Streaming SSR 的核心价值

传统 SSR 必须等待整棵组件树渲染完毕后才能输出 HTML——如果其中一个请求耗时较长,整个页面的首字节时间都会被拖慢。Streaming SSR 通过 HTTP 分块传输,让首字节在第一个组件渲染完成后立刻发出,其余部分随渲染完成逐步推送给浏览器。

App Router 默认启用 Streaming——你不需要额外配置,只需用 Suspense 包裹异步组件:


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

// 快速内容 ——
export default function BlogPage() {
  return (
    <>
      
{/* 立即渲染,无等待 */}
}> {/* 异步渲染,流式推送 */}
{/* 立即渲染 */} &> ); }

在这个例子中,HeaderFooter 的 HTML 会在首个分块中发出,让浏览器立刻开始渲染页面的框架;ArticleListTrendingTopicsRecentComments 各自独立地异步加载,每完成一个就推送一个分块。

4.2 用 loading.tsx 实现整页骨架屏

App Router 的 loading.tsx 会自动包裹同级的 page.tsx,在数据获取期间显示骨架屏:


// app/blog/loading.tsx
export default function BlogLoading() {
  return (
    <>
      
{[1,2,3,4,5].map(i => (
))}
&> ); }

当路由切换时,loading.tsx 的内容会立刻替换掉上一页的内容,配合 Suspense 实现渐进式加载——用户永远看不到白屏。

4.3 让流式效果最大化的并行模式

Streaming SSR 配合并行数据获取,可以将页面 LCP(Largest Contentful Paint)性能提升到极致:


// app/dashboard/analytics/page.tsx
import { Suspense } from 'react';
import { ChartSkeleton } from './ChartSkeleton';

// 慢查询 —— 3秒的数据库聚合计算
async function getYearlyRevenue() {
  await new Promise(r => setTimeout(r, 3000));
  return db.selectFrom('transactions')...execute();
}

// 快查询 —— 200ms
async function getDailyVisits() {
  return db.selectFrom('visits')...execute();
}

export default function AnalyticsPage() {
  // 两个异步组件各自独立 Suspense
  // 页面首字节发出 <200ms>
      

分析仪表盘

}> }>
&> ); }

第五章:Server Actions——API Routes 的替代品

5.1 Server Actions 解决什么问题

在 Pages Router 中,前端表单提交需要经历:定义 API Route(pages/api/xxx.ts)→ 前端写 fetch('/api/xxx') → 手动处理序列化和错误。这个模式在 App Router 中被 Server Action 彻底简化——可以直接在组件中调用服务端函数


// app/actions/posts.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
import { auth } from '@clerk/nextjs/server';
import { z } from 'zod';

const PostSchema = z.object({
  title: z.string().min(3).max(200),
  content: z.string().min(10),
  category: z.string()
});

export async function createPost(form[removed] 50vw"
        className="object-cover"
      />
    
); }

8.3 元数据 API——SEO 声明式配置

App Router 的 Metadata API 让 SEO 配置变得声明式,且支持动态生成:

 {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage, width: 1200, height: 630 }],
      type: 'article',
      publishedTime: post.createdAt.toISOString(),
      authors: [post.authorName]
    },
    alternates: {
      canonical: `https://example.com/blog/${post.slug}`
    }
  };
}

第九章:从 Pages Router 到 App Router 的迁移策略

9.1 渐进式迁移——两条路由共存

Next.js 允许 App Router 和 Pages Router 在同一个项目中并存,这意味着你可以按页面逐个迁移,而不是一次性重写:

Next.js 会自动处理路由优先级:App Router 优先匹配,未匹配的路径回退到 Pages Router。两者共存期间,共享的 Layout 会在 App Router 侧的根 Layout 之外再包裹——因此建议你先在 Pages Router 的 _document.tsx 中把全局样式抽离成 CSS 文件引入,而不是依赖 React 的 styled-jsx。

9.2 重点替换的 API 对照表

Pages Router App Router 关键差异
getServerSidePropsasync Server Component无需包装函数,直接在组件顶层 await
getStaticProps默认静态渲染 + fetch(revalidate)默认行为,无需配置
getInitialProps不推荐使用App Router 无此 API
nookies() / headers()cookies() / headers()变成 async 函数,调用会标记路由为动态
useRouter()useRouter() from 'next/navigation'新的 router.push() 会复用预取数据
API RoutesRoute Handlers + Server Actions表单推荐走 Server Action,REST 接口保留 Route Handler

总结:App Router 的设计哲学

Next.js App Router 的核心设计思路可以归结为三个关键词:

默认在服务端——让组件默认运行在服务端,只有真正需要交互的部分才标记为客户端组件。这让大多数页面(列表页、详情页、文章页等)实现零客户端 JS。

数据靠近组件——每个路由段拥有自己的数据获取逻辑,和 UI 共处一个目录。文件共置(co-location)让页面和它的数据源保持内聚,避免了传统 BFF 层的分散维护。

渐进式增强——从纯 HTML 开始,按需叠加 Streaming、Suspense、Server Actions 等能力。不支持 JS 的浏览器也能展示完整的 HTML 内容,交互功能作为增强层加载。

掌握了这些原则后,App Router 就能让你在更少的代码量下构建出更快、更易维护的 Web 应用。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部