6月5日 21:49

Next.js App Router vs Pages Router:核心区别和渐进式迁移指南

Next.js 13 引入的 App Router 不是 Pages Router 的替代品——它是一套全新的架构,基于 React Server Components,改变了数据获取、渲染和路由的整个思维方式。迁移不是改个目录名那么简单。

核心区别一表看懂

维度Pages RouterApp Router
目录pages/app/
路由文件pages/about.jsapp/about/page.tsx
API 路由pages/api/users.jsapp/api/users/route.ts
布局_app.js + 唯一 layout嵌套 layout(每个目录可以有)
数据获取getServerSideProps / getStaticProps组件内直接 async/await
渲染模型客户端组件为主服务端组件为主(RSC)
状态天然客户端服务端组件无状态

数据获取:最大的变化

Pages Router 方式

typescript
// pages/users.tsx export async function getServerSideProps() { const users = await fetchUsers() return { props: { users } } } export default function UsersPage({ users }) { return <UserList users={users} /> }

每个页面需要单独的 getServerSidePropsgetStaticProps 函数,数据只能在页面级获取,子组件不能自己获取。

App Router 方式

typescript
// app/users/page.tsx export default async function UsersPage() { const users = await fetchUsers() // 直接在组件里 await return <UserList users={users} /> }

不需要特殊的 getXXXProps 函数——组件本身就是 async 函数,直接 await 获取数据。子组件也能自己获取:

typescript
// components/user-stats.tsx export default async function UserStats() { const stats = await fetchStats() // 子组件自己获取数据 return <div>活跃用户:{stats.active}</div> }

这是 RSC 的核心优势:组件级数据获取,不再需要在页面顶层把所有数据攒齐再一层层传 props。

布局系统:从全局到嵌套

Pages Router:一个布局管所有页面

typescript
// pages/_app.tsx - 全局唯一的布局 export default function App({ Component, pageProps }) { return ( <Layout> <Component {...pageProps} /> </Layout> ) }

所有页面共享同一个 _app.js。想做"某些页面有侧边栏,某些没有"很别扭——要靠条件判断或把布局塞进每个页面。

App Router:嵌套布局,每个目录可以有

shell
app/ layout.tsx # 根布局(必须有 html + body) page.tsx # 首页 dashboard/ layout.tsx # dashboard 专属布局(侧边栏) page.tsx # dashboard 首页 settings/ page.tsx # settings 页(继承 dashboard 布局)
typescript
// app/dashboard/layout.tsx export default function DashboardLayout({ children }) { return ( <div className="flex"> <Sidebar /> <main className="flex-1">{children}</main> </div> ) }

/dashboard/settings 会渲染三层布局:根 layout → dashboard layout → settings page。切换 settings 页面时,dashboard layout 不会重新渲染——侧边栏状态保留。

这是 App Router 布局的核心:嵌套且持久化。Pages Router 做不到这一点——每次路由切换都重新挂载整个页面。

Server Components vs Client Components

这是迁移时最容易搞混的概念:

Server Components(默认)

typescript
// app/page.tsx - 自动是 Server Component export default async function Page() { const data = await fetch('https://api.example.com/data') return <div>{data.title}</div> }
  • 在服务端渲染,不会发送 JS 到客户端
  • 可以直接访问数据库、文件系统
  • 不能用 useStateuseEffect、浏览器 API
  • 不能绑定事件处理函数(onClick 等)

Client Components(需要显式声明)

typescript
'use client' // 这行声明是 Client Component import { useState } from 'react' export default function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount(count + 1)}>{count}</button> }
  • 在客户端渲染(可以 hydration)
  • 可以用所有 React Hooks
  • 可以绑定事件、使用浏览器 API
  • 不能直接 await 异步数据

选择原则

需要选择
获取数据Server Component
访问后端资源Server Component
useState/useEffectClient Component
事件处理(onClick)Client Component
浏览器 API(window)Client Component

默认用 Server Component,只在需要交互时才用 Client Component。 这和 Pages Router 的思维完全相反——Pages Router 默认全是客户端组件。

API 路由的变化

Pages Router

typescript
// pages/api/users.ts import type { NextApiRequest, NextApiResponse } from 'next' export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'GET') { res.status(200).json({ users: [] }) } }

App Router

typescript
// app/api/users/route.ts import { NextResponse } from 'next/server' export async function GET() { const users = await fetchUsers() return NextResponse.json({ users }) } export async function POST(request: Request) { const body = await request.json() const user = await createUser(body) return NextResponse.json(user, { status: 201 }) }

变化:从单个 handler 函数 + if (req.method) 拆成独立的方法函数。每个 HTTP 方法一个导出函数,更清晰。

迁移策略

不要一次性全迁移——两个 Router 可以共存:

1. 渐进式迁移

shell
pages/ old-page.tsx # 还在用 Pages Router app/ new-page/ page.tsx # 新页面用 App Router

Next.js 同时支持两种 Router。新页面用 App Router,旧页面保持不动,逐步迁移。

2. 迁移优先级

  1. API 路由:最简单,改动最小
  2. 纯展示页面:不需要交互,直接改成 Server Component
  3. 有交互的页面:拆成 Server + Client 组件混合

3. 常见迁移坑

  • useRouter 不一样了:Pages Router 用 next/router,App Router 用 next/navigation
  • getServerSideProps 变成组件内 async:不再需要序列化/反序列化 props
  • _app.js 里的 Provider:移到 app/layout.tsx,但要包在 Client Component 里
  • 图片组件next/image API 有变化,注意 fill 属性替代了 layout="fill"

什么时候还该用 Pages Router

不是所有项目都需要迁移:

  • 老项目稳定运行:没有性能问题,不需要 RSC,不必迁移
  • 团队不熟悉 RSC:App Router 的思维模式完全不同,迁移成本不只是改代码
  • 重度依赖客户端状态:如果你的页面几乎全是客户端交互,RSC 的优势体现不出来

App Router 的核心价值是服务端渲染优先 + 嵌套布局。如果你的项目确实受益于这两点,值得迁移;否则 Pages Router 继续用就好——Next.js 没有废弃它的计划。

标签:Next.js