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+ 是大型(需要制定季度计划)。

数据获取模式:统计 getServerSidePropsgetStaticPropsgetStaticPathsgetInitialProps 的使用次数。每个都需要转换为 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 Routespages/api/ 中的路由需要迁移到 app/api/ 的 Route Handler,接口签名完全不同。

2. 渐进式迁移策略

2.1 核心原则:共存,而非替换

App Router 最大的设计优势是与 Pages Router 完全共存app/pages/ 可以同时存在于一个项目中,Next.js 会分别处理两个路由系统的请求。

这意味着你不需要一次性迁移——可以一个路由一个路由地搬,旧的继续在 pages/ 运行,新的在 app/ 中构建。直到所有路由迁移完成,再删除 pages/ 目录。

但有一个关键限制:同一个路径不能同时存在于 app/pages/ 中。比如你不能同时有 pages/about.tsxapp/about/page.tsx——Next.js 会报错。迁移时必须先删除 pages/ 中的文件,再在 app/ 中创建对应的文件。

2.2 四阶段迁移计划

第一阶段:基础设施(1-3 天)

搭建 App Router 的骨架,不迁移任何业务路由:

  1. 创建 app/layout.tsx,把 _app.tsx_document.tsx 中的内容合并进来。这是 App Router 的根布局,替代了 Pages Router 的 _app + _document 组合。
  2. 设置 metadata API 替代 next/headnext-seo
  3. 把 Provider 层级(主题、国际化、认证、React Query)搬到 app/layout.tsx 中。注意:Provider 通常是客户端组件,需要创建一个 providers.tsx 客户端组件来包裹。
  4. 配置 middleware.ts(如果有的话,位置不变,但内容可能需要调整)。

第二阶段:低风险路由(1-2 周)

选择最简单的页面先迁移,建立信心和流程:

优先迁移的候选:

  • 纯静态页面(About、Terms、Privacy)——没有数据获取,只需把 pages/about.tsx 变成 app/about/page.tsx
  • getStaticProps 页面——直接变成 Server Component 中的 fetchdb.query,最接近 1:1 转换。
  • 布局相似的页面组——如果有一组页面共享相同的侧边栏布局,迁移时可以一起处理,建立 layout.tsx

每迁移一个路由,走完整的流程:删除 pages/ 文件 → 创建 app/ 文件 → 功能验证 → 回归测试 → 部署。

第三阶段:核心路由(2-4 周)

迁移有复杂数据获取的页面:

  • getServerSideProps 页面 → Server Component + 直接数据获取(每次请求都执行)
  • getStaticPaths + getStaticProps 的动态路由 → generateStaticParams + Server Component
  • 需要客户端交互的页面 → 拆分为 Server Component(数据获取层)+ Client Component(交互层)

这个阶段最容易出问题。常见陷阱:

  • 忘记拆分客户端组件:把 useStateuseEffect、事件处理器留在 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.tsxpages/ 目录。
  • 清理不再需要的兼容代码和依赖。
  • 全面回归测试。

2.3 迁移顺序的优先级矩阵

优先级页面类型原因
P0布局和全局组件所有页面的基础
P1高流量静态页收益最大、风险最低
P2SSG 页面转换简单
P3SSR 页面需要重新设计数据获取
P4有复杂交互的页面需要仔细拆分 SC/CC
P5API 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 返回 &#123; paths, fallback &#125; 对象,generateStaticParams 返回参数数组。fallback 行为通过 dynamicParams 配置项控制。

getInitialProps → 不推荐直接映射

getInitialProps 是最老的数据获取方式,它同时运行在服务端(首次请求)和客户端(客户端导航)。App Router 中没有直接对应物。迁移策略:服务端逻辑放到 Server Component,客户端逻辑放到 Client Component 的 useEffect 或 React Query。

3.2 路由系统映射

Pages RouterApp Router说明
pages/index.tsxapp/page.tsx首页
pages/about.tsxapp/about/page.tsx必须用文件夹 + page.tsx
pages/blog/[slug].tsxapp/blog/[slug]/page.tsx动态路由
pages/blog/[...slug].tsxapp/blog/[...slug]/page.tsxCatch-all 路由
pages/_app.tsxapp/layout.tsx根布局
pages/_document.tsxapp/layout.tsx合并到根布局
pages/404.tsxapp/not-found.tsx404 页面
pages/500.tsxapp/error.tsx错误页面
pages/_error.tsxapp/error.tsx + global-error.tsx全局错误处理
pages/api/xxx.tsapp/api/xxx/route.tsAPI Route

3.3 元数据映射

Pages Router 使用 next/headnext-seo 管理页面元数据。App Router 引入了专门的 Metadata API。

Pages RouterApp Router
&lt;Head&gt;&lt;title&gt;...&lt;/title&gt;&lt;/Head&gt;export const metadata = &#123; title: '...' &#125;
next-seoDefaultSeoapp/layout.tsx 中的 metadata
动态 title(基于数据)export async function generateMetadata()
&lt;Head&gt;&lt;link rel="icon" /&gt;&lt;/Head&gt;app/favicon.ico 文件约定
&lt;Head&gt;&lt;meta og:image /&gt;&lt;/Head&gt;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 中,paramssearchParams 变成了 Promise,需要 await。Pages Router 中 query 是同步的。这个变化影响每一个动态路由页面。

客户端导航行为:Pages Router 的 router.push 等方法从 next/router 导入。App Router 使用 next/navigationuseRouter,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.tscompiler.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 的默认行为(不缓存),确保功能正确。然后逐步添加缓存——先从流量最高、变化最少的页面开始,使用 revalidateunstable_cache

5.5 "一次性大规模迁移"

大爆炸迁移(一次把所有路由从 pages/ 搬到 app/)风险极高:如果出了问题,你不知道是哪个路由引起的;回滚意味着撤销所有变更。

渐进式迁移才是正道。每次迁移 1-3 个路由,部署后观察监控(错误率、性能指标),确认稳定后再继续。

6. 回归测试策略

6.1 测试金字塔

迁移不改变业务逻辑,只改变渲染方式和数据获取方式。测试重点是:

  • 功能回归:迁移前后页面行为一致——表单提交、导航跳转、数据展示、权限控制
  • SEO 回归&lt;title&gt;&lt;meta&gt;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&lt;Suspense&gt; 中包裹慢速数据获取的组件,页面骨架先到用户浏览器,数据到了再流式替换。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 无缓存、getStaticPathsgenerateStaticParams
  • 常见陷阱:滥用 'use client'、忽视缓存变化、一次性大规模迁移、缺少错误边界
  • 回归测试:每个路由一份验证清单 + E2E 自动化 + 灰度发布 + 监控
  • 迁移后优化:Streaming SSR、PPR、Server Action、React 19 特性