什么是 App Router?
App Router 是 Next.js 13+ 引入的新路由系统与应用架构,使用 app/ 目录进行约定式路由(文件即路由),并以 React Server Components(RSC) 为默认能力,重新组织了布局(Layout)、页面(Page)、加载态(Loading)、错误处理(Error)与数据获取方式。
如果你把 Pages Router 看作“以页面为中心(pages/)”,那么 App Router 更像“以路由段(route segment)与布局树为中心(app/)”:每个路由段都可以拥有自己的布局、加载与错误边界,最终拼装成完整的 UI 树。
为什么重要?
- 更强的布局与导航体验:支持嵌套布局、持久化布局与局部刷新,减少重复渲染与重复请求。
- 默认 Server-first(更省 JS):页面默认是 Server Component,可把更多渲染与数据读取放到服务端,减少客户端 bundle。
- 更好的加载与错误边界:每个路由段可独立定义
loading.tsx/error.tsx,提升用户体验与可维护性。 - 更自然的全栈能力:Route Handlers(
app/api/**/route.ts)、Server Actions 让“同仓全栈”更顺滑。 - 为未来特性铺路:并行路由、拦截路由、Streaming/Suspense 等能力,在 App Router 下更完整。
核心概念/工作原理
路由段与约定文件
在 App Router 中,一个 URL 通常由 app/ 下的目录结构决定:
page.tsx:该段的页面入口layout.tsx:该段与其子段共享的布局(可嵌套)loading.tsx:该段的加载 UI(基于 Suspense)error.tsx:该段的错误边界(必须是 Client Component)not-found.tsx:该段的 404 页面
一个最小示例:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}
// app/page.tsx
export default function HomePage() {
return <main>首页</main>;
}
Server Components vs Client Components
- 默认是 Server Component:不写
"use client"的组件会在服务端渲染,可直接访问服务器资源(数据库、私有 API、环境变量等)。 - 需要交互才用 Client Component:例如按钮点击、表单即时校验、使用
useState/useEffect等。
// app/dashboard/page.tsx (Server Component,默认)
import { getServerSession } from "@/lib/auth"; // 示例:服务端读取会话
export default async function DashboardPage() {
const session = await getServerSession();
return <div>欢迎你,{session.user.name}</div>;
}
// app/dashboard/ClientButton.tsx (Client Component)
"use client";
import { useState } from "react";
export function ClientButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>点击:{count}</button>;
}
嵌套布局与共享 UI
App Router 的布局是“树状组合”的。你可以为某一段提供独立布局,同时继承上层布局:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard-shell">
<aside>侧边栏</aside>
<section>{children}</section>
</div>
);
}
数据获取与缓存(直觉版)
在 App Router 中,常见推荐是:
- 服务端组件直接获取数据(更简单)
- 对外部
fetch默认做缓存与去重(可按需设置cache/revalidate) - 需要“提交后更新 UI”,常用 Server Actions 或配合
revalidatePath/revalidateTag
(更深入可看 nextjs-server-actions、nextjs-server-components 相关词条。)
实际应用示例
场景 1:构建一个带加载态与错误边界的详情页(动态路由)
// app/posts/[slug]/page.tsx
import { notFound } from "next/navigation";
async function getPost(slug: string) {
// 示例:从你的 API/数据库读取
const res = await fetch(`https://example.com/api/posts/${slug}`, { cache: "no-store" });
if (res.status === 404) return null;
if (!res.ok) throw new Error("加载失败");
return res.json();
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// app/posts/[slug]/loading.tsx
export default function Loading() {
return <div>正在加载文章...</div>;
}
// app/posts/[slug]/error.tsx
"use client";
export default function Error({ reset }: { reset: () => void }) {
return (
<div>
<p>加载文章时出错。</p>
<button onClick={() => reset()}>重试</button>
</div>
);
}
解释:page.tsx 在服务端执行并获取数据;当请求未完成时展示 loading.tsx;抛错会被 error.tsx 捕获;找不到资源用 notFound() 进入该段的 404 逻辑。
场景 2:用 Layout 让“账户区”拥有统一外壳与权限校验
// app/account/layout.tsx
import { redirect } from "next/navigation";
import { getServerSession } from "@/lib/auth"; // 示例
export default async function AccountLayout({ children }: { children: React.ReactNode }) {
const session = await getServerSession();
if (!session) redirect("/login");
return (
<div className="account">
<header>账户中心</header>
<main>{children}</main>
</div>
);
}
解释:把“鉴权 + 统一外壳”放到 layout.tsx,可以保证 app/account/** 下的页面都自动受保护,同时减少每个页面重复写权限判断。
最佳实践
💡 建议 1:默认使用 Server Component,只有在需要交互时才下沉到 Client。 这能显著减少客户端 JS,并让数据访问更直观。
- 把共享 UI 放在布局层:导航、侧边栏、权限校验、全局提供器等优先写在
layout.tsx。 - 为关键路由段补齐边界文件:给核心页面加
loading.tsx/error.tsx/not-found.tsx,用户体验会提升一个量级。 - 动态路由与数据获取要“可预期”:明确哪些页面需要实时数据(
no-store),哪些可以增量更新(revalidate)。 - 让路由结构表达业务:例如
app/(marketing)、app/(app)这类 route group(括号目录)可以让 URL 不变但结构更清晰。
⚠️ 常见坑:在 Server Component 里直接使用浏览器 API 或 React hooks(useState/useEffect)。 需要交互的组件请加
"use client"并隔离边界。
常见问题
App Router 与 Pages Router 必须二选一吗?
答案:可以共存。你可以逐步迁移,把新功能放到 app/,旧页面继续留在 pages/,直到迁移完成。
为什么我的 error.tsx 必须是 Client Component?
答案:因为错误边界依赖客户端错误恢复机制(例如 reset()),Next.js 要求 error.tsx 加 "use client"。
layout.tsx 会在每次导航都重新渲染吗?
答案:通常不会。布局可以持久化(尤其是同一布局树下的导航),这是 App Router 提升体验与性能的重要原因之一。
什么时候需要 Route Handlers(app/api/**/route.ts)?
答案:当你需要提供 HTTP API(给第三方回调、Webhook、移动端或外部系统调用)时,Route Handlers 很合适;仅用于表单提交时也可以考虑 Server Actions。
与其他概念的对比
| 特性 | App Router | Pages Router |
|---|---|---|
| 路由目录 | app/ | pages/ |
| 默认渲染模式 | Server Components 优先 | 客户端/服务端混合(以页面为中心) |
| 布局能力 | 嵌套布局、持久化更强 | 需要手动封装与组合 |
| Loading/Error 边界 | 路由段级别原生支持 | 需要自行实现 |
| 新特性承载 | 并行/拦截路由、Server Actions 等 | 支持有限或需要额外方案 |
进一步学习
相关术语
nextjs-server-componentsnextjs-server-actionsnextjs-middlewarenextjs-dynamic-routesnextjs-parallel-routesnextjs-metadata-api