Pages Router 到 App Router 完整迁移手册
Pages Router 并没有被废弃,它仍然是受支持的路由方案。但 App Router 代表了 Next.js 的未来方向——Server Component、Streaming、Partial Prerendering、Server Action——这些能力只在 App Router 中可用。如果你的项目需要长期维护和演进,迁移是迟早要面对的事。本章提供一份经过生产验证的渐进式迁移手册,帮你把风险降到最低。
1. 迁移前的认知准备
1.1 为什么要迁移
App Router 的核心优势不是语法更新,而是架构范式的升级。
Pages Router 的本质是"页面级渲染"——每个页面要么是 SSR(getServerSideProps),要么是 SSG(getStaticProps),整个页面是一个渲染单元。你不能在同一个页面中混合静态和动态内容,除非用客户端 useEffect 补数据。
App Router 引入了"组件级渲染"——同一个页面中,不同组件可以有不同的渲染策略。Header 是静态的、内容区域是动态的、侧边栏是 ISR 的,它们可以并行渲染、独立缓存。这不是渐进式的改进,而是根本性的架构变化。
迁移带来的具体收益:
- 性能:Server Component 零 JS Bundle,Streaming SSR 减少 TTFB,Partial Prerendering 混合静态 + 动态。在典型 SaaS Dashboard 场景下,迁移后首屏 JS 减少 30-50%。
- 开发体验:Server Action 替代 API Route + 客户端 fetch 的双层代码,数据获取直接写在组件内而非
getServerSideProps里,代码更内聚。 - 架构能力:嵌套布局(Layout)天然支持,不再需要
_app.tsx的全局包裹和手动状态传递;Parallel Routes 和 Intercepting Routes 实现复杂 UI 模式。 - 长期维护:新特性(PPR、Turbopack 深度集成、React 19 特性)只在 App Router 中可用,Pages Router 进入维护模式。
1.2 什么时候不应该迁移
迁移有成本,不是所有项目都值得。以下情况建议暂缓:
- 项目即将下线:剩余生命周期 < 6 个月,迁移投入无法回收。
- 团队对 React Server Component 缺乏理解:RSC 是 App Router 的基石,如果团队没有理解"服务端组件 vs 客户端组件"的心智模型,迁移会变成灾难。建议先用 2-3 周做知识储备。
- 重度依赖不兼容的库:某些 CSS-in-JS 库(styled-components 需要额外配置、Emotion 的 SSR 支持有限)、某些状态管理方案(MobX 不兼容 RSC)。迁移前必须验证依赖兼容性。
- 项目极其稳定,无功能迭代需求:如果只是修 Bug,没有新功能开发,迁移的 ROI 不高。
1.3 迁移规模评估
在动手之前,先做一次全面的评估。
路由数量:统计 pages/ 目录下有多少路由文件。10 个以下是小项目(1-2 周),10-50 个是中型(1-2 个月),50+ 是大型(需要制定季度计划)。
数据获取模式:统计 getServerSideProps、getStaticProps、getStaticPaths、getInitialProps 的使用次数。每个都需要转换为 App Router 的对应模式。getInitialProps 是最难迁移的,因为它同时运行在服务端和客户端。
全局状态依赖:检查 _app.tsx 中有多少 Provider 层级。Redux、React Query、主题、国际化、认证——每一层都需要在 App Router 中重新规划放置位置。
自定义 _document.tsx:检查是否有自定义的 HTML 结构、字体加载、CSS-in-JS 的 SSR 注入。这些需要迁移到 app/layout.tsx 和 Next.js 内置的 font/metadata API。
API Routes:pages/api/ 中的路由需要迁移到 app/api/ 的 Route Handler,接口签名完全不同。
2. 渐进式迁移策略
2.1 核心原则:共存,而非替换
App Router 最大的设计优势是与 Pages Router 完全共存。app/ 和 pages/ 可以同时存在于一个项目中,Next.js 会分别处理两个路由系统的请求。
这意味着你不需要一次性迁移——可以一个路由一个路由地搬,旧的继续在 pages/ 运行,新的在 app/ 中构建。直到所有路由迁移完成,再删除 pages/ 目录。
但有一个关键限制:同一个路径不能同时存在于 app/ 和 pages/ 中。比如你不能同时有 pages/about.tsx 和 app/about/page.tsx——Next.js 会报错。迁移时必须先删除 pages/ 中的文件,再在 app/ 中创建对应的文件。
2.2 四阶段迁移计划
第一阶段:基础设施(1-3 天)
搭建 App Router 的骨架,不迁移任何业务路由:
- 创建
app/layout.tsx,把_app.tsx和_document.tsx中的内容合并进来。这是 App Router 的根布局,替代了 Pages Router 的_app+_document组合。 - 设置
metadataAPI 替代next/head或next-seo。 - 把 Provider 层级(主题、国际化、认证、React Query)搬到
app/layout.tsx中。注意:Provider 通常是客户端组件,需要创建一个providers.tsx客户端组件来包裹。 - 配置
middleware.ts(如果有的话,位置不变,但内容可能需要调整)。
第二阶段:低风险路由(1-2 周)
选择最简单的页面先迁移,建立信心和流程:
优先迁移的候选:
- 纯静态页面(About、Terms、Privacy)——没有数据获取,只需把
pages/about.tsx变成app/about/page.tsx。 getStaticProps页面——直接变成 Server Component 中的fetch或db.query,最接近 1:1 转换。- 布局相似的页面组——如果有一组页面共享相同的侧边栏布局,迁移时可以一起处理,建立
layout.tsx。
每迁移一个路由,走完整的流程:删除 pages/ 文件 → 创建 app/ 文件 → 功能验证 → 回归测试 → 部署。
第三阶段:核心路由(2-4 周)
迁移有复杂数据获取的页面:
getServerSideProps页面 → Server Component + 直接数据获取(每次请求都执行)- 有
getStaticPaths+getStaticProps的动态路由 →generateStaticParams+ Server Component - 需要客户端交互的页面 → 拆分为 Server Component(数据获取层)+ Client Component(交互层)
这个阶段最容易出问题。常见陷阱:
- 忘记拆分客户端组件:把
useState、useEffect、事件处理器留在 Server Component 中,导致编译报错。 - 过度使用
'use client':把整个页面标记为客户端组件,失去 Server Component 的性能优势。正确做法是只在最小必要范围内使用'use client'。 - 数据获取时机变化:
getServerSideProps是在路由级别执行的,App Router 中数据获取可以在任何 Server Component 中执行。这改变了数据流的心智模型。
第四阶段:API Routes 和清理(1-2 周)
- 把
pages/api/迁移到app/api/的 Route Handler。 - 删除
_app.tsx、_document.tsx、pages/目录。 - 清理不再需要的兼容代码和依赖。
- 全面回归测试。
2.3 迁移顺序的优先级矩阵
| 优先级 | 页面类型 | 原因 |
|---|---|---|
| P0 | 布局和全局组件 | 所有页面的基础 |
| P1 | 高流量静态页 | 收益最大、风险最低 |
| P2 | SSG 页面 | 转换简单 |
| P3 | SSR 页面 | 需要重新设计数据获取 |
| P4 | 有复杂交互的页面 | 需要仔细拆分 SC/CC |
| P5 | API Routes | 最后处理,接口签名不同 |
3. 核心概念映射
3.1 数据获取映射
这是迁移中最关键的转换——几乎每个页面都涉及。
getStaticProps → Server Component(默认缓存)
Pages Router 中,getStaticProps 在构建时执行,返回的 props 传给页面组件。App Router 中,Server Component 本身就在服务端执行,直接在组件体内获取数据即可。Next.js 15 默认不缓存 fetch,如果需要 SSG 行为,需要显式设置 cache: 'force-cache' 或使用 unstable_cache。
getServerSideProps → Server Component(无缓存)
概念上最直接的映射。getServerSideProps 每次请求执行,App Router 的 Server Component 默认也是每次请求执行(Next.js 15 的默认行为)。直接在 Server Component 中查询数据库或调用 API 即可。
getStaticPaths → generateStaticParams
功能完全对等。getStaticPaths 返回 { paths, fallback } 对象,generateStaticParams 返回参数数组。fallback 行为通过 dynamicParams 配置项控制。
getInitialProps → 不推荐直接映射
getInitialProps 是最老的数据获取方式,它同时运行在服务端(首次请求)和客户端(客户端导航)。App Router 中没有直接对应物。迁移策略:服务端逻辑放到 Server Component,客户端逻辑放到 Client Component 的 useEffect 或 React Query。
3.2 路由系统映射
| Pages Router | App Router | 说明 |
|---|---|---|
pages/index.tsx | app/page.tsx | 首页 |
pages/about.tsx | app/about/page.tsx | 必须用文件夹 + page.tsx |
pages/blog/[slug].tsx | app/blog/[slug]/page.tsx | 动态路由 |
pages/blog/[...slug].tsx | app/blog/[...slug]/page.tsx | Catch-all 路由 |
pages/_app.tsx | app/layout.tsx | 根布局 |
pages/_document.tsx | app/layout.tsx | 合并到根布局 |
pages/404.tsx | app/not-found.tsx | 404 页面 |
pages/500.tsx | app/error.tsx | 错误页面 |
pages/_error.tsx | app/error.tsx + global-error.tsx | 全局错误处理 |
pages/api/xxx.ts | app/api/xxx/route.ts | API Route |
3.3 元数据映射
Pages Router 使用 next/head 或 next-seo 管理页面元数据。App Router 引入了专门的 Metadata API。
| Pages Router | App Router |
|---|---|
<Head><title>...</title></Head> | export const metadata = { title: '...' } |
next-seo 的 DefaultSeo | app/layout.tsx 中的 metadata |
| 动态 title(基于数据) | export async function generateMetadata() |
<Head><link rel="icon" /></Head> | app/favicon.ico 文件约定 |
<Head><meta og:image /></Head> | app/opengraph-image.tsx(可以用 JSX 动态生成) |
3.4 全局状态和 Provider 映射
Pages Router 中,所有 Provider 都包在 _app.tsx 里:
// Pages Router: pages/_app.tsx
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider>
<AuthProvider>
<QueryProvider>
<Component {...pageProps} />
</QueryProvider>
</AuthProvider>
</ThemeProvider>
)
}App Router 中,Provider 需要放在客户端组件中,然后在 layout.tsx 里使用:
// App Router: app/providers.tsx
'use client'
export function Providers({ children }) {
return (
<ThemeProvider>
<AuthProvider>
<QueryProvider>
{children}
</QueryProvider>
</AuthProvider>
</ThemeProvider>
)
}
// App Router: app/layout.tsx (Server Component)
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html><body>
<Providers>{children}</Providers>
</body></html>
)
}关键区别:layout.tsx 本身是 Server Component,但 Provider 通常需要客户端 Context(如 Theme、Auth),所以必须提取到客户端组件中。这是迁移中最容易犯错的地方。
4. Breaking Changes 详解
4.1 路由行为变化
默认缓存行为:这是迁移中最容易踩的坑。
Pages Router 中,getServerSideProps 总是在每次请求时执行——没有缓存歧义。App Router 在不同版本中有不同的默认行为:
- Next.js 14:
fetch默认缓存(force-cache),导致很多开发者发现页面数据不更新 - Next.js 15+:
fetch默认不缓存(no-store),行为更符合直觉
如果你从 Pages Router 迁移到 Next.js 15+,数据获取行为基本与 getServerSideProps 一致——每次请求都重新获取。需要缓存时显式声明。
路由参数异步化:Next.js 15 中,params 和 searchParams 变成了 Promise,需要 await。Pages Router 中 query 是同步的。这个变化影响每一个动态路由页面。
客户端导航行为:Pages Router 的 router.push 等方法从 next/router 导入。App Router 使用 next/navigation 的 useRouter,API 有细微差别——没有 router.events,用 usePathname + useSearchParams 替代。
4.2 CSS 和样式
CSS Modules:完全兼容,无需修改。
CSS-in-JS:这是迁移中的痛点。Server Component 不支持运行时 CSS-in-JS(如 styled-components 的默认模式、Emotion 的 SSR),因为这些库需要 React Context 和运行时的 JavaScript。
解决方案:
- 把使用 CSS-in-JS 的组件标记为
'use client' - 或者迁移到 Tailwind CSS / CSS Modules(推荐,零运行时)
- styled-components 可以通过
next.config.ts的compiler.styledComponents配置让 Next.js 在编译时处理
Tailwind CSS:完全兼容,是 App Router 的推荐方案。
4.3 中间件和认证
Pages Router 中常用 getServerSideProps 做页面级认证检查。App Router 中推荐在 middleware.ts 中统一处理认证路由保护,或者在 layout.tsx 中检查。
认证库的兼容性:
- NextAuth.js / Auth.js:v5 原生支持 App Router
- Clerk:完全兼容
- Supabase Auth:需要使用
@supabase/ssr包 - 自研 JWT:middleware 中验证 token,通过 cookies 传递
5. 常见迁移陷阱
5.1 "把 'use client' 贴满整个项目"
这是最常见的错误。当开发者遇到 Server Component 的限制(不能用 useState、不能用 onClick、不能用 useEffect)时,最直接的反应是在文件顶部加 'use client'。
如果项目中 80% 的组件都是 Client Component,那你就失去了 App Router 的核心价值——和 Pages Router 相比没有任何性能优势,还增加了复杂度。
正确做法:重新思考组件的职责。一个典型的页面应该是——
- 页面组件(Server Component):负责数据获取、布局组织
- 交互组件(Client Component):只包含需要浏览器 API 的最小单元
比如一个产品列表页:整个页面是 Server Component,数据在服务端查询;只有"加入购物车"按钮、搜索输入框、筛选下拉框是 Client Component。
5.2 "在 Server Component 中使用 useEffect 获取数据"
从 Pages Router 迁移过来的开发者习惯了这种模式:组件挂载后用 useEffect + fetch 获取数据。在 App Router 中,这种模式完全没有必要——Server Component 可以直接 await 数据。
把数据获取从客户端搬到服务端,TTFB 会增加(因为服务端要等数据),但 LCP 会改善(HTML 中直接包含数据,不需要等 JS 加载 + 执行 + 请求)。整体用户感知速度更快。
5.3 "没有建立错误边界"
Pages Router 中,_error.tsx 捕获所有错误。App Router 要求每个路由段都可以有自己的 error.tsx,并且根布局的错误需要 global-error.tsx 来捕获。
迁移时如果忘记设置错误边界,一旦出现运行时错误,用户看到的是白屏而非友好的错误页面。
建议:至少设置以下错误边界——
app/error.tsx:捕获应用级错误app/global-error.tsx:捕获根布局错误app/not-found.tsx:自定义 404 页面- 关键业务路由的
error.tsx:如app/(dashboard)/error.tsx
5.4 "忽视了缓存行为的变化"
Pages Router 中缓存很直觉——getStaticProps 就是静态,getServerSideProps 就是动态。App Router 的缓存是多层的:请求级(fetch 缓存)、路由级(Full Route Cache)、数据级(unstable_cache)、客户端级(Router Cache)。
迁移后常见的症状是"页面数据不更新"或"每次都重新请求性能下降"。这往往是因为不理解新的缓存模型。
建议:迁移初期,先用 Next.js 15 的默认行为(不缓存),确保功能正确。然后逐步添加缓存——先从流量最高、变化最少的页面开始,使用 revalidate 或 unstable_cache。
5.5 "一次性大规模迁移"
大爆炸迁移(一次把所有路由从 pages/ 搬到 app/)风险极高:如果出了问题,你不知道是哪个路由引起的;回滚意味着撤销所有变更。
渐进式迁移才是正道。每次迁移 1-3 个路由,部署后观察监控(错误率、性能指标),确认稳定后再继续。
6. 回归测试策略
6.1 测试金字塔
迁移不改变业务逻辑,只改变渲染方式和数据获取方式。测试重点是:
- 功能回归:迁移前后页面行为一致——表单提交、导航跳转、数据展示、权限控制
- SEO 回归:
<title>、<meta>、og:image、canonical URL、结构化数据 - 性能回归:Core Web Vitals 不应劣化(LCP、INP、CLS)
- 无障碍回归:DOM 结构变化可能影响屏幕阅读器
6.2 每个路由的验证清单
每迁移一个路由,逐项检查:
□ 页面正常渲染,内容与迁移前一致
□ 动态数据正确加载(刷新页面验证 SSR/SSG)
□ 客户端导航正常(从其他页面点进来)
□ 直接访问 URL 正常(SSR 场景)
□ 搜索引擎可见内容正确(查看页面源码)
□ 错误状态正确显示(传入无效参数、网络错误)
□ 加载状态正确显示(loading.tsx 或 Suspense)
□ 认证保护正常(未登录跳转、权限检查)
□ 响应式布局正常
□ 浏览器前进/后退正常
6.3 自动化回归
如果有 Playwright 或 Cypress 测试,迁移后跑一遍端到端测试是最有效的验证手段。如果没有自动化测试,迁移是建立它们的好时机——至少为核心流程写 E2E 测试。
推荐的 E2E 覆盖范围:
- 首页渲染 + SEO meta
- 登录/注册流程
- 核心业务流程(如创建项目、提交表单)
- 404 和错误页面
- 移动端响应式
6.4 监控和灰度
大型项目建议使用灰度发布。通过 Vercel 的 Preview Deployment 或者 Nginx 的流量分割,先把 10% 的流量导到迁移后的版本,观察错误率和性能指标,确认无异常后逐步扩大比例。
关键监控指标:
- 服务端错误率(500 响应)
- 客户端 JS 错误率
- Core Web Vitals(LCP、INP、CLS)
- 页面转化率(如果适用)
7. 大型项目迁移案例
7.1 电商项目(200+ 路由)
背景:一个运营 3 年的电商平台,200+ 页面路由,50+ API Route,使用 Redux + styled-components。
策略:
- 第 1 个月:基础设施搭建 + 将 styled-components 替换为 Tailwind CSS(这个工作量占了总迁移的 40%)
- 第 2 个月:迁移营销页面(首页、分类页、商品详情页),这些是流量最大的页面,收益最高
- 第 3 个月:迁移用户中心(订单、地址、个人资料)
- 第 4 个月:迁移管理后台 + API Routes + 清理
关键决策:
- Redux → Zustand + Server Component 直查数据:Redux 在 App Router 中仍然可用,但很多全局状态可以用 Server Component 的数据获取替代
- styled-components → Tailwind CSS:先做样式迁移,再做路由迁移,避免同时改两件事
结果:JS Bundle 减少 45%,LCP 改善 30%,服务端渲染的 TTFB 略有增加(+50ms),但整体用户体验提升明显。
7.2 SaaS Dashboard(80+ 路由)
背景:B2B SaaS 产品,80 个路由,React Query + Tailwind CSS,Next.js 13 Pages Router。
策略:
- 第 1 周:搭建
app/layout.tsx,Provider 迁移 - 第 2-3 周:迁移公共页面(定价页、文档页、博客)
- 第 4-6 周:迁移 Dashboard 路由——利用嵌套 Layout 重构侧边栏布局
- 第 7 周:API Routes + Server Action 替代部分 React Query 调用
- 第 8 周:全面测试 + 上线
关键发现:
- 嵌套 Layout 是 App Router 对 Dashboard 类应用最大的价值——侧边栏不再重新渲染,页面切换更流畅
- React Query 在 App Router 中仍然有用——用于需要轮询、乐观更新的场景;但简单的数据获取可以直接用 Server Component 替代
8. 迁移后的优化机会
迁移完成后,App Router 解锁了一些 Pages Router 无法实现的优化:
Streaming SSR:在 loading.tsx 或 <Suspense> 中包裹慢速数据获取的组件,页面骨架先到用户浏览器,数据到了再流式替换。TTFB 大幅改善。
Partial Prerendering(PPR):Next.js 15+ 的实验性功能,把页面的静态部分在构建时生成,动态部分在请求时流式填充。对于 Dashboard 类页面(布局是静态的,数据是动态的),这是性能的终极形态。
Server Action:用 Server Action 替代 API Route + 客户端 fetch 的组合,减少代码量,类型安全贯穿前后端。
React 19 特性:useOptimistic(乐观更新)、useFormStatus(表单状态)、use(在客户端组件中消费 Promise)——这些只在 App Router 中可用。
本章小结
- 迁移动机:App Router 不是语法升级,而是架构范式变化——从页面级渲染到组件级渲染
- 渐进式策略:
app/和pages/共存,逐路由迁移,每次部署验证 - 四阶段:基础设施 → 低风险路由 → 核心路由 → API Routes 和清理
- 核心映射:
getStaticProps→ Server Component 默认行为、getServerSideProps→ Server Component 无缓存、getStaticPaths→generateStaticParams - 常见陷阱:滥用
'use client'、忽视缓存变化、一次性大规模迁移、缺少错误边界 - 回归测试:每个路由一份验证清单 + E2E 自动化 + 灰度发布 + 监控
- 迁移后优化:Streaming SSR、PPR、Server Action、React 19 特性