App Router 基础
本教程共 42 篇 · 第 3 篇 · 更新于 2026-07-30 · 约 7 分钟阅读
3. App Router 基础
本节目标:搞懂 App Router 的核心思想——文件即路由,学会用 layout 和 page 搭建页面骨架。
App Router vs Pages Router
Next.js 有两套路由系统:
Pages Router(旧版)
- 在
pages/目录下创建文件 - 用
getServerSideProps、getStaticProps获取数据 - 页面级组件直接导出
App Router(新版,推荐)
- 在
app/目录下创建文件 - 组件默认是 Server Components,直接用 async/await 获取数据
- 用
layout.tsx实现共享布局 - 支持 React Server Components、流式渲染等新特性
本教程全程使用 App Router。Next.js 16 的所有新特性都围绕 App Router 设计。
文件即路由
App Router 的核心规则:文件夹定义路由,文件定义 UI。
app/
├── page.tsx # /
├── about/
│ └── page.tsx # /about
└── blog/
├── page.tsx # /blog
└── [slug]/
└── page.tsx # /blog/:slug
每个路由段由文件夹层级决定。只有包含 page.tsx 的路径才是公开可访问的。
创建 Page
Page 是特定路由的 UI。在 app 目录里新建 page.tsx,默认导出一个 React 组件:
// app/page.tsx
export default function Page() {
return <h1>Hello Next.js!</h1>
}
// app/about/page.tsx
export default function Page() {
return <h1>About Us</h1>
}
Page 可以是异步组件,直接在里面获取数据:
// app/blog/page.tsx
export default async function Page() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json())
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
创建 Layout
Layout 是多个页面共享的 UI。导航时,Layout 保持状态、不重新渲染。
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>导航栏</header>
<main>{children}</main>
<footer>页脚</footer>
</body>
</html>
)
}
根布局(Root Layout)是必须的,必须包含 <html> 和 <body> 标签。
children 会被替换为当前路由对应的 page 或嵌套 layout。
嵌套路由
创建嵌套路由,只需嵌套文件夹:
app/
├── layout.tsx # 根布局
├── page.tsx # /
└── blog/
├── layout.tsx # blog 专属布局
├── page.tsx # /blog
└── [slug]/
└── page.tsx # /blog/:slug
访问 /blog/my-post 时,渲染顺序是:
app/layout.tsx(根布局)app/blog/layout.tsx(blog 布局)app/blog/[slug]/page.tsx(文章页面)
每一层 layout 包裹下一层的输出。
嵌套 Layout
Layout 可以嵌套,子 layout 被包裹在父 layout 的 children 中:
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<h2>博客频道</h2>
{children}
</section>
)
}
最终渲染结果:
<html>
<body>
<header>导航栏</header>
<main>
<section>
<h2>博客频道</h2>
<!-- 这里是 page.tsx 的内容 -->
</section>
</main>
<footer>页脚</footer>
</body>
</html>
动态路由段
用方括号创建动态路由:
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
ImportantNext.js 16 写法:
params是一个 Promise,需要用await解包。
旧版写法(v14 及之前):params是同步对象,直接解构。
searchParams
Page 可以接收 URL 查询参数:
// app/products/page.tsx
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters
// 用 filters 过滤产品列表
}
使用 searchParams 会让页面进入动态渲染模式——因为查询参数只有在请求时才能确定。
什么时候用什么
searchParamsprop:查询参数用于加载页面数据(分页、筛选)useSearchParamshook:查询参数只在客户端使用(过滤已加载的列表)
组件层级
特殊文件的渲染顺序是固定的:
layout.tsx
└── template.tsx
└── error.tsx
└── loading.tsx
└── not-found.tsx
└── page.tsx
这意味着:
layout包裹所有子组件error捕获下方组件的渲染错误loading在子组件加载时显示page是最内层,直接面向用户
不能做的事
Layout 有几个限制:
- 不能访问 searchParams——因为 layout 不重新渲染,searchParams 会过期
- 不能访问 pathname——同理,导航后 pathname 会变
- 不能向 children 传数据——但可以在同一路由多次获取数据,Next.js 会自动去重
如果需要这些功能,在 Page 里用 searchParams,或者用 Client Component 配合 usePathname、useSearchParams。
小结
这一章我们掌握了 App Router 的核心概念:
- 文件即路由——文件夹层级对应 URL
page.tsx定义页面 UIlayout.tsx定义共享布局,不重新渲染- 动态路由用
[slug],params是 Promise searchParams用于读取查询参数
下一章,我们来学习路由进阶技巧——路由组、并行路由和拦截路由。