Next.js App Router 入门指南:核心概念与基础使用详解

引言
说实话,我第一次打开 Next.js 官方文档的时候是懵的。
左边侧栏里,“Pages Router” 和 “App Router” 并排站着,像是在说”随你选”。可问题来了——选哪个?两个有啥区别?我该学哪个?文档也没直接告诉我答案,反而越看越乱。教程里有的用 pages 文件夹,有的用 app 文件夹,代码写法还完全不一样。
后来我才搞明白:Next.js 其实有两套完全不同的路由系统。老的叫 Pages Router,稳定可靠,但有些新功能用不了。新的叫 App Router,从 v13 开始推出,v13.4 正式稳定,现在已经是官方推荐的方向了。
你可能会问:“那我还要学 App Router 吗?会不会又是一个折腾人的新东西?”
这篇文章就是想帮你解决这个困惑。我会用最简单的方式,把 App Router 的核心概念讲清楚——什么是 Server Components、特殊文件怎么用、跟 Pages Router 到底差在哪。看完你就能快速上手,少走弯路。
什么是 App Router?为什么要用它?
简单来说,App Router 是 Next.js 在 v13 推出的新路由系统。它基于 React 的最新特性——Server Components(服务器组件),设计更现代、更灵活。
跟老的 Pages Router 比,有三个明显的好处:
1. 性能更好
App Router 默认使用服务器组件。这意味着大部分代码在服务器端运行,用户浏览器下载的 JavaScript 变少了,页面加载自然快。根据 Vercel 2024 年的报告,超过 60% 的顶级 Next.js 应用已经切换到 App Router 了。
"超过 60% 的顶级 Next.js 应用已经切换到 App Router"
2. 布局系统更灵活
Pages Router 里,要实现嵌套布局比较麻烦。App Router 直接用 layout.js 文件就能搞定,页面切换时布局还不会重新渲染——体验丝滑。
3. 错误处理和加载状态更强大
你可以用 loading.js 文件定义加载动画,用 error.js 捕获错误并显示降级UI。这些在 Pages Router 里要自己手写,App Router 直接给你约定好了。
那 Pages Router 还能用吗?能。
两套系统可以共存,但如果你现在要学 Next.js,我建议直接上手 App Router。它是官方推荐的方向,新项目脚手架(v14.1.4 开始)默认就用 App Router 了。
文件系统路由:从目录到页面
App Router 最核心的概念是:你的文件夹结构就是你的路由结构。
听起来有点抽象,看个例子就懂了:
app/
├── page.js # 首页,对应 /
├── about/
│ └── page.js # 关于页,对应 /about
└── blog/
├── page.js # 博客列表,对应 /blog
└── [slug]/
└── page.js # 博客详情,对应 /blog/:slug有几个关键点要记住:
1. page.js 是路由的入口
只有叫 page.js 的文件才会成为可访问的页面。其他文件(比如 layout.js、loading.js)都是配套的功能文件,不会直接被访问。
2. 动态路由用方括号
想要 /blog/hello-world 这样的动态路由?创建 app/blog/[slug]/page.js,参数 slug 会自动传给你的组件:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>文章:{params.slug}</h1>
}3. 捕获所有路由用 [...slug]
有时候你需要匹配多层路径,比如 /docs/a/b/c。用 app/docs/[...slug]/page.js 就行,params.slug 会是个数组 ['a', 'b', 'c']。
对比 Pages Router:
如果你之前用过 Pages Router,会发现它用的是 pages/blog/[id].js。App Router 改成了 app/blog/[id]/page.js,多了一层文件夹。为啥?为了给每个路由留出空间放 layout.js、loading.js 这些特殊文件。
刚开始可能觉得麻烦,但习惯了就会发现——整个项目结构清晰多了。
Server Components vs Client Components:核心概念
这可能是 App Router 里最让人困惑的概念了。我刚开始学的时候也懵圈了好久。
说白了就一句话:App Router 里的组件默认在服务器端运行,只在需要交互时才在浏览器端运行。
默认是 Server Component
在 app/ 目录下创建的组件,默认都是 Server Component(服务器组件)。它们在服务器端渲染完,直接把 HTML 发给浏览器。
好处很明显:
- JavaScript 体积小:代码不用发给浏览器,用户下载的 JS 文件更小
- 可以直接访问后端资源:数据库查询、API 密钥这些敏感信息可以放心用
- 首屏加载快:服务器渲染好直接给你,FCP(首次内容渲染)时间短
举个例子,这就是个典型的 Server Component:
// app/products/page.js
// 这是 Server Component,在服务器端运行
async function getProducts() {
const res = await fetch('https://api.example.com/products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>产品列表</h1>
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
)
}看到没?可以直接 async/await 获取数据,不需要 useEffect 或者 getServerSideProps。
什么时候用 Client Component?
但有些场景必须在浏览器里运行,比如:
- 要用 React hooks(
useState、useEffect) - 要处理用户交互(
onClick、onChange) - 要用浏览器 API(
localStorage、window)
这时候就需要 Client Component(客户端组件)。标记很简单,文件顶部加一行 'use client':
// components/AddToCartButton.js
'use client' // 标记为 Client Component
import { useState } from 'react'
export default function AddToCartButton({ productId }) {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
加入购物车 ({count})
</button>
)
}混合使用:最佳实践
真正厉害的是,你可以把两种组件混着用。
比如一个产品页面:
- 产品列表用 Server Component(服务器端获取数据,减少 JS 体积)
- 加购按钮用 Client Component(需要处理点击交互)
// app/products/page.js (Server Component)
import AddToCartButton from '@/components/AddToCartButton' // Client Component
async function getProducts() {
// 服务器端获取数据
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>产品列表</h1>
{products.map(p => (
<div key={p.id}>
{p.name}
<AddToCartButton productId={p.id} />
</div>
))}
</div>
)
}记住一个原则:默认用 Server Component,够用了。只在确实需要交互时才用 'use client'。
别一上来就给所有组件加 'use client'——那跟没用 App Router 有啥区别?
特殊文件:让项目更专业
App Router 定义了一堆特殊文件名——layout.js、loading.js、error.js 这些。刚看到时可能觉得麻烦,但用起来真的香。
layout.js:共享布局
这是最常用的特殊文件。它定义了一个路由段的布局,会包裹同级和子级的所有页面。
比如你想给整个应用加个导航栏和页脚:
// app/layout.js (根布局)
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body>
<nav>导航栏</nav>
<main>{children}</main>
<footer>页脚</footer>
</body>
</html>
)
}更厉害的是,你可以嵌套布局:
app/
├── layout.js # 全局布局(导航+页脚)
├── page.js # 首页
└── dashboard/
├── layout.js # 仪表盘布局(侧边栏)
├── page.js # /dashboard
└── settings/
└── page.js # /dashboard/settings当你从 /dashboard 跳到 /dashboard/settings 时,全局布局和仪表盘布局都不会重新渲染——只有 page.js 会更新。丝滑。
loading.js:加载状态
不用自己写 useState 管理 loading 了。创建一个 loading.js,App Router 会自动用 Suspense 包裹你的页面:
// app/dashboard/loading.js
export default function Loading() {
return <div>加载中...</div>
}页面数据获取时,loading.js 的内容会自动显示。就这么简单。
error.js:错误边界
用来捕获页面错误并显示降级UI:
// app/dashboard/error.js
'use client' // Error boundaries 必须是 Client Component
export default function Error({ error, reset }) {
return (
<div>
<h2>出错了:{error.message}</h2>
<button onClick={reset}>重试</button>
</div>
)
}有个坑要注意:error.js 无法捕获同级 layout.js 的错误。为啥?React Error Boundary 的限制——它只能捕获子组件的错误,不能捕获自己或父级的错误。
要捕获 layout.js 的错误,得在父级目录放 error.js,或者用根目录的 global-error.js。
not-found.js:404 页面
当路由不存在时显示:
// app/not-found.js
export default function NotFound() {
return <h1>页面不存在</h1>
}你也可以在代码里主动触发 404:
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
if (!post) notFound() // 触发 not-found.js
return <article>{post.title}</article>
}文件层级关系
这些特殊文件有固定的层级关系:
layout.js
├── loading.js (Suspense 边界)
│ └── page.js
└── error.js (Error 边界)layout 在最外层,error.js 包不住它。loading.js 负责加载状态,error.js 负责错误处理。
搞清楚这个层级,就不会踩坑了。
数据获取:告别 getServerSideProps
如果你用过 Pages Router,肯定写过 getServerSideProps 或 getStaticProps。老实说,那套 API 挺别扭的——要单独导出一个函数,数据传递也不直观。
App Router 把这一切简化了。
直接用 async/await
在 Server Component 里,你可以直接在组件函数里获取数据:
// app/posts/page.js
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}看到没?就是普通的 async/await,不需要特殊的 API。
并行数据获取
更厉害的是,你可以并行获取多个数据源:
export default async function Dashboard() {
// 并行获取,不会阻塞
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
])
return (
<div>
<h1>{user.name}</h1>
<Posts data={posts} />
<Stats data={stats} />
</div>
)
}数据缓存和重新验证
Next.js 会自动缓存 fetch 请求。你可以控制缓存策略:
// 缓存 60 秒后重新验证
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// 不缓存,每次都获取最新数据
fetch('https://api.example.com/data', {
cache: 'no-store'
})对比 Pages Router:
- Pages Router:
getServerSideProps+getStaticProps,要单独导出函数 - App Router:直接
async/await,组件内获取数据
简单多了吧?
初学者常见问题与解决方案
学 App Router 的时候,我踩过不少坑。这里整理了几个最常见的问题,希望能帮你少走弯路。
问题1:什么时候用 ‘use client’?
困惑点:看到教程里到处都是 'use client',不知道什么时候该加。
解决方案:
记住一个原则——默认不加,需要时再加。
只在以下情况加 'use client':
- 用了 React hooks(
useState、useEffect、useContext) - 有用户交互(
onClick、onChange) - 用了浏览器 API(
window、localStorage)
其他时候别加。Server Component 性能更好,能直接访问后端资源。
问题2:layout.js 和 page.js 的关系?
困惑点:这俩文件放在同一个文件夹,谁包裹谁?
解决方案:layout.js 包裹 page.js 和子路由。
app/
├── layout.js # 包裹下面所有页面
├── page.js # 首页,被上面的 layout 包裹
└── about/
└── page.js # 关于页,也被上面的 layout 包裹页面切换时,layout.js 不会重新渲染——只有 page.js 更新。这就是为什么导航栏不会闪一下。
问题3:动态路由参数怎么拿?
困惑点:创建了 [slug]/page.js,但不知道怎么获取 slug 的值。
解决方案:
通过 params 属性获取:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
console.log(params.slug) // 就是 URL 里的那个值
return <h1>文章:{params.slug}</h1>
}如果是嵌套动态路由,比如 app/blog/[category]/[slug]/page.js:
export default function Post({ params }) {
console.log(params.category, params.slug)
return <h1>{params.category} - {params.slug}</h1>
}问题4:error.js 不生效?
困惑点:创建了 error.js,但布局出错时没有捕获。
解决方案:error.js 无法捕获同级 layout.js 的错误。这是 React Error Boundary 的限制。
要捕获布局错误,有两个办法:
- 在父级目录放
error.js - 在根目录用
global-error.js(需要包含<html>和<body>标签)
// app/global-error.js
'use client'
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>全局错误:{error.message}</h2>
<button onClick={reset}>重试</button>
</body>
</html>
)
}问题5:我的旧项目要不要迁移?
困惑点:看到 App Router 这么多新东西,担心旧项目要全部重写。
解决方案:
不用急。
Pages Router 和 App Router 可以共存。你可以:
- 旧功能继续用
pages/ - 新功能用
app/
Vercel 官方也说了,Pages Router 会长期支持,不会废弃。
但如果是新项目,直接用 App Router。它是未来方向,生态会越来越好。
结论
说了这么多,咱们快速回顾一下 App Router 的五个核心概念:
- 文件系统路由:文件夹结构就是路由结构,
page.js是入口 - Server Components:默认在服务器端运行,性能更好
- Client Components:需要交互时用
'use client'标记 - 特殊文件:
layout.js、loading.js、error.js让项目更专业 - 数据获取:直接用
async/await,告别getServerSideProps
App Router 确实是 Next.js 的未来方向。Vercel 在持续投入,社区也在积极跟进。如果你现在开始学 Next.js,直接上手 App Router 准没错。
接下来该干嘛?
动手试试。创建一个小项目,试着用 App Router 做个博客或者待办事项应用。概念再多,不如自己敲一遍代码来得清楚。
遇到问题别慌——Pages Router 和 App Router 可以共存,实在搞不定就先用 Pages Router,慢慢再迁移。
最后,Next.js 官方文档虽然有点乱,但 App Router 部分写得还算详细。遇到具体问题,记得去翻翻文档,或者上 GitHub Discussions 搜搜看。
祝你学习顺利!
常见问题
什么时候应该使用 'use client'?
layout.js 和 page.js 的关系是什么?
如何获取动态路由参数?
error.js 为什么无法捕获 layout.js 的错误?
旧项目需要迁移到 App Router 吗?
App Router 和 Pages Router 的主要区别是什么?
11 分钟阅读 · 发布于: 2025年12月18日 · 修改于: 2026年1月22日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南

Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战

Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南


评论
使用 GitHub 账号登录后即可评论