The App Router introduced in Next.js 13 is the biggest architectural shift since the framework's birth. Built on React Server Components, it redefines how frontend and backend code is organized. For teams with many Pages Router projects, understanding the App Router's core concepts is the first step of migration.
Directory Structure and Convention-Based Routing
App Router 使用 app/ 目录,每个文件夹代表一个路由段。与 Pages Router 最大的不同是:page.tsx 只负责页面渲染,布局、加载态、错误处理各有专门文件。
app/
├── layout.tsx // 根布局,所有页面共享
├── page.tsx // 首页 /
├── loading.tsx // 首页加载态
├── error.tsx // 首页错误边界
├── blog/
│ ├── layout.tsx // blog 路由的布局
│ ├── page.tsx // /blog
│ └── [slug]/
│ └── page.tsx // /blog/:slug
└── api/
└── route.ts // API 路由
每个 page.tsx 默认是 Server Component,这意味着你可以在里面直接 await 数据请求,无需 useEffect。
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
interface Props {
params: { slug: string }
}
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 } // ISR: 60秒重新验证
})
if (!res.ok) return null
return res.json()
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug)
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Layout System and State Preservation
layout.tsx 是 App Router 最实用的特性之一。布局在路由切换时不会重新渲染,这意味着嵌套在布局中的状态可以跨页面保持。
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
import { Navbar } from '@/components/Navbar'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'My Blog',
description: 'A senior frontend blog'
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<Navbar />
<main>{children}</main>
</body>
</html>
)
}
关键区别:根布局必须包含 <html> 和 <body> 标签。布局不会在导航时重新挂载,但 page.tsx 会。这个特性天然解决了之前 Sidebar 重新渲染导致状态丢失的问题。
Parallel Routes and Intercepting Routes
App Router 支持用 @folder 实现并行路由,用 (.)folder 实现拦截路由。这两个特性组合起来,可以轻松实现 Modal 弹窗 + 背景保持的交互模式。
// app/layout.tsx - 并行路由插槽
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
// app/@modal/(.)photo/[id]/page.tsx - 拦截路由
import { Modal } from '@/components/Modal'
import { getPhoto } from '@/lib/api'
export default async function PhotoModal({
params,
}: {
params: { id: string }
}) {
const photo = await getPhoto(params.id)
return (
<Modal>
<img src={photo.url} alt={photo.title} />
</Modal>
)
}
这种模式在 Pages Router 中实现非常复杂,需要手动管理 History API 和状态。App Router 把它变成了约定。
Data Fetching and Caching Strategies
App Router 的 fetch 被 Next.js 做了扩展,新增了 cache 和 next.revalidate 选项。默认行为是请求级去重 + 永久缓存,这与 Pages Router 完全不同。
// 永久缓存(默认)
const data = await fetch('https://api.example.com/data')
// 60秒 ISR
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// 每次请求都获取新数据
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
对于非 fetch 的数据源(数据库、ORM),需要使用 unstable_cache API:
import { unstable_cache } from 'next/cache'
const getCachedPosts = unstable_cache(
async () => {
return db.posts.findMany({ orderBy: { createdAt: 'desc' } })
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
Summary
- App Router 基于 React Server Components,默认组件在服务端运行,减少客户端 JS 体积
- 布局系统解决了跨页面状态保持问题,
loading.tsx和error.tsx让 UX 更完善 - 并行路由 + 拦截路由将复杂交互(如 Modal)变成了约定,大幅降低实现成本
- 缓存策略默认激进,需要根据业务场景主动选择
revalidate或no-store - 目前 App Router 已经稳定,新项目建议直接使用,存量项目可以渐进式迁移
