引言:为什么 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 (
<>
{/* 立即渲染,无等待 */}
}>
{/* 异步渲染,流式推送 */}
在这个例子中,Header 和 Footer 的 HTML 会在首个分块中发出,让浏览器立刻开始渲染页面的框架;ArticleList、TrendingTopics 和 RecentComments 各自独立地异步加载,每完成一个就推送一个分块。
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
关键差异
getServerSideProps async Server Component 无需包装函数,直接在组件顶层 await
getStaticProps 默认静态渲染 + fetch(revalidate) 默认行为,无需配置
getInitialProps 不推荐使用 App Router 无此 API
nookies() / headers() cookies() / headers() 变成 async 函数,调用会标记路由为动态
useRouter() useRouter() from 'next/navigation' 新的 router.push() 会复用预取数据
API Routes Route 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 条评论

发表评论 取消回复