这两年做 Next.js 开发几乎绕不开同一个纠结项目该用 Pages Router 还是 App Router。从我实际做过的项目来看这个问题不是“哪套路由更好用”这么简单而是两套路由背后的设计思路、数据获取方式、组件边界逻辑完全不一样。官方文档写得清楚但很多人照着文档写没问题一遇到老项目迁移、混合路由、权限校验这类真实场景就卡壳。我把 Pages Router 和 App Router 放在一起做了个系统对比也把从老路由迁到新路由时实打实踩过的坑整理出来这篇就当是给正在选型、准备迁移的朋友的一份参考。1. 两套路由系统的底层设计差异1.1 Pages Router文件路径即路由代码结构简单直接Pages Router 是 Next.js 从早期版本就带进来的设计。你只需要在pages目录下创建文件文件路径就会自动映射成 URL 地址。比如pages/about.js对应/aboutpages/blog/[slug].js对应/blog/xxx不需要额外配置路由表。这个设计特别像后端框架里“约定优于配置”的思路上手成本极低这也是过去几年大量 Next.js 项目都跑在 Pages Router 上的原因。Pages Router 的核心是页面组件。每个页面除了导出组件本身还可以导出一系列数据获取函数比如getStaticProps、getServerSideProps。渲染模式跟着页面走一个页面要么静态生成、要么服务端渲染逻辑非常直白。配合_app.js和_document.js两个全局文件所有页面都能被统一包上布局、注入全局样式或第三方脚本。但 Pages Router 的问题也很明显它没有真正的嵌套布局概念。虽然你可以在_app.js里包一层公共框架但不同路由层级想要不同布局就得靠组件组合手动套代码一多就容易乱。还有一个隐患是_app.js是全局入口所有页面都会经过它一旦在这里做了数据获取或复杂逻辑整个站点的性能都会受影响。早期项目规模小的时候感受不深等页面多了、团队大了维护成本就开始往上走。1.2 App Router目录结构即组件树路由与 UI 深度绑定App Router 是 Next.js 13 引入的新路由模型设计思路变得非常激进app目录下不再是一个文件对应一个页面而是通过文件夹和约定文件组合定义路由。page.tsx定义页面layout.tsx定义该层级的布局loading.tsx定义加载态error.tsx定义错误边界。每个文件夹都可以拥有自己独立的布局和加载、错误处理子路由会自动继承并嵌套在父级布局之内。这套设计把路由和 React 组件树直接绑定在一起。你在app目录里看到的文件夹层级基本就等于页面上实际渲染出来的 UI 层级。比如一个后台系统需要“侧边栏 顶栏 内容区”的结构用 App Router 写就是建一个dashboard文件夹在里面放一个layout.tsx搞定所有dashboard下的子页面自动继承这套框架不需要每个页面都去引用一遍布局组件。另一个核心变化是Server Component 成为默认。App Router 下的页面组件默认是服务端组件可以直接await数据、直接访问数据库不再需要getServerSideProps这种独立的数据获取层。这从根本上改变了 Next.js 的写法习惯。我刚开始用的时候很不适应总觉得服务端组件限制多后来才慢慢体会到它在减少客户端 JavaScript 体积、提升首屏性能上的价值。两套路由的核心思路对比如下对比维度Pages RouterApp Router路由定义pages目录下文件即路由app目录下文件夹 约定文件名动态路由[slug].js、[...slug].js[slug]文件夹行为与 Pages 类似嵌套布局无原生概念靠_app和组件组合layout.tsx天然嵌套数据获取getStaticProps/getServerSideProps服务端组件内直接awaitfetch缓存控制加载态手动维护组件内部 loading 状态loading.tsx自动包裹 Suspense错误处理_error.js全局处理error.tsx按路由层级细化全局入口_app.js/_document.js根layout.tsx承担大部分职责API 路由pages/api/xxx.jsapp/api/xxx/route.ts路由间跳转next/routernext/navigation2. 核心写法对照同一功能在两套路由里怎么实现2.1 页面、动态路由和嵌套布局的改写从 Pages 迁到 App最直观的感受是所有东西的位置都换了。拿博客文章页举例Pages Router 时代我们是这样写的// pages/blog/[slug].js import { useRouter } from next/router export default function Post({ post }) { const router useRouter() const { slug } router.query return article{/* 渲染文章内容 */}/article } export async function getServerSideProps({ params }) { const res await fetch(https://api.example.com/posts/${params.slug}) const post await res.json() return { props: { post } } }同样的页面在 App Router 里是这样写的// app/blog/[slug]/page.tsx export default async function PostPage({ params }: { params: Promise{ slug: string } }) { const { slug } await params const res await fetch(https://api.example.com/posts/${slug}, { cache: no-store }) const post await res.json() return article{/* 渲染文章内容 */}/article }注意几个关键差异。第一params在 App Router 的 Next.js 15 里变成了Promise需要await才能拿到真正的动态路由参数这是新版异步请求 API 的一部分。第二数据获取不再用独立的getServerSideProps而是直接在组件里await fetch因为这是服务端组件代码会在服务器上执行完再发送 HTML 给浏览器。第三useRouter不再从next/router导入而是改用next/navigation很多老代码迁移时第一件事就是改这个导入路径。嵌套布局的差别更明显。Pages Router 时代我想让一个后台系统的所有页面都带上侧边栏只能在_app.js里手动包一层或者写一个Layout组件然后在每个页面里手动调用。App Router 的做法是建一个dashboard文件夹并在里面放一个layout.tsx// app/dashboard/layout.tsx export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( div classNameflex aside后台侧边栏/aside main{children}/main /div ) }只要文件放在app/dashboard/下所有子路由都会自动渲染到main里。而且布局组件在子路由切换时不会重新渲染意味着布局里的状态比如侧边栏折叠状态可以跨页面保留这在 Pages Router 里很难做到。2.2 API 路由、全局文件和页面元信息的迁移API 路由的写法也完全不同。Pages Router 时代pages/api下每个文件导出一个handler函数通过req、res手动判断 HTTP 方法App Router 则改为route.ts文件直接按方法名导出函数。同样的用户接口两种写法对比如下// pages/api/user.js export default function handler(req, res) { if (req.method GET) { res.status(200).json({ name: 某开发者 }) } else if (req.method POST) { // 处理新建逻辑 } }// app/api/user/route.ts export async function GET(request: Request) { return Response.json({ name: 某开发者 }) } export async function POST(request: Request) { // 处理新建逻辑 return Response.json({ success: true }) }新写法明显更干净每个方法职责单一类型推导也更友好。但要注意route.ts只能处理 API 逻辑不能在同一个路径下再放page.tsx否则路由会冲突。全局文件方面_app.js里写的全局 Provider、全局样式、认证逻辑迁移时可以拆到根layout.tsx里_document.js里自定义的html、body属性也基本可以在根布局中通过html标签直接控制。还有旧代码里常见的next/head手动设置页面标题App Router 里换成了更简洁的 metadata 导出// app/blog/page.tsx export const metadata { title: 博客列表, description: 这里是我的博客文章索引页, }metadata既支持静态导出也支持generateMetadata函数做动态生成相比next/head统一了 SEO 相关的管理方式。3. 渲染模式与数据获取两套路由差别最大的地方3.1 Pages Router 时代的数据获取套路Pages Router 把渲染模式分得很细getStaticProps对应静态生成getServerSideProps对应服务端渲染getInitialProps是最早的兼容方案后来基本不再推荐使用。用哪种模式页面就是在构建时生成还是在请求时实时渲染开发者必须在写代码时明确选一个。这个模型看着清晰但用起来有几个别扭的地方。数据获取逻辑和组件本身是分离的组件里接收props但props从哪来、什么时候更新要看外层函数怎么写。页面一旦用useRouter做客户端跳转路由变化时getServerSideProps不会重新执行需要额外写客户端事件处理。还有一个常见体验getServerSideProps执行期间用户只能看到一个空白页面或一个无意义的 loading 状态因为数据获取和页面渲染是两个阶段中间缺少组件层面的加载态控制。当时我们做一个内容管理系统首页轮播图、侧边栏、列表数据都要通过getServerSideProps并行加载每个数据源都要手动写Promise.all还要考虑某一个接口挂了不能整页崩溃代码量非常可观。这种场景下Pages Router 对复杂页面的处理能力确实有限。3.2 App Router 的服务端组件与缓存控制App Router 把“渲染模式”这个问题换了个角度处理。服务端组件是默认选项你在组件里写的await fetch()默认会在服务器上执行数据直接注入页面 HTML不再区分getStaticProps还是getServerSideProps。真正需要动态更新的数据通过fetch的缓存参数控制// 每次请求都获取最新数据 const res await fetch(https://api.example.com/todos, { cache: no-store }) // 每 60 秒重新生成一次类似 ISR const res2 await fetch(https://api.example.com/posts, { next: { revalidate: 60 } }) // 构建时获取静态化处理 const res3 await fetch(https://api.example.com/config)这套设计把数据缓存的粒度细化到了每个请求级别而不是整个页面级别。同一个页面里部分数据可以静态生成部分数据可以实时获取不需要为了一个动态模块牺牲整页的静态化性能。配合loading.tsxApp Router 还给服务端组件的数据加载提供了原生加载态。页面在等待数据时会自动渲染loading.tsx指定的内容相当于自动给你包了一个Suspense边界。我实际体验下来视觉上更像一个流畅的渐进式加载而不是整页白屏等接口。客户端组件和服务端组件的边界也很明确默认都在服务端跑组件顶部写了use client之后它和它引用的子组件才会被当作客户端组件处理。这种“服务端优先、客户端按需”的思维和 Pages Router 时代“所有组件都是客户端组件、只有数据获取在服务端”的模型是完全相反的适应起来需要一段时间。4. 迁移踩坑实录我从老路由迁到 App Router 遇到的典型问题4.1 容易翻车的几个写法问题忘加use client。服务端组件里不能直接用useState、useEffect、useRouter这些客户端专属 API一旦用了控制台就会报错提示你某个组件只能在 Client Component 里使用。我第一次迁移登录页面时就是忘了加这行排查了半个多小时。记住一个原则能用服务端组件就用服务端组件只有需要交互、状态、浏览器 API 的组件才加use client。params和searchParams是 Promise。Next.js 15 把动态路由参数和查询参数都改成了异步 API。老代码里直接params.slug的地方迁移后都要先await params。我有一个模块刚开始迁移时忘了改页面渲染出来一直是空内容控制台也不报错排查半天才发现是params还没解包。layout.tsx拿不到动态路由参数。布局组件是为了在子路由切换时保持不重渲染所以它无法读取当前动态路由段。比如app/blog/[slug]/layout.tsx里想要通过params.slug做一些样式判断是拿不到的。解决方法是把布局改成template.tsx后者会在每次导航时重新挂载可以访问到当前参数。useRouter导入来源写错。App Router 的路由跳转 hook 在next/navigation里Pages Router 的next/router在 App Router 下不能使用。这个报错看起来很直接但我见过不少人把两份代码混在一起导致router.push后页面不刷新。4.2 其它值得注意的坑路由冲突问题一定要留意。Next.js 允许pages和app目录并存方便渐进式迁移但同一个 URL 不能同时被两套路由定义比如pages/about.js和app/about/page.tsx同时存在控制台会直接报错提示路由重复。理论上app目录优先级更高但这类冲突很容易造成线上 404迁移时先检查一遍路由清单再动手。app目录下新增文件特别多很多初看没用。不只是page.tsxlayout.tsx、loading.tsx、error.tsx、not-found.tsx每个都有自己的职责。error.tsx目前是必须做成客户端组件的因为它依赖 React 的错误边界机制not-found.tsx用于自定义 404而loading.tsx在上线后如果忘了删除会导致生产环境出现不必要的加载闪烁。创建文件之前先想清楚这个路由层级真正需要哪些约定文件。并行路由和拦截路由的默认 UI 也要注意。在 App Router 里并行路由用slot文件夹表示比如同时渲染主内容区和侧边栏详情拦截路由用(.)前缀表示比如从列表页点击进入详情页时弹出 Modal。这两种路由在刷新页面时都可能出现找不到页面内容的问题必须在对应插槽目录下提供default.tsx兜底否则刷新后直接 404。以下是我整理的常见问题速查表迁移时可以直接对照症状常见原因处理方式useState/useEffect报错组件缺少use client声明在文件顶部添加use client动态路由参数为空Next.js 15 的params是 Promise未 awaitconst { slug } await params布局里读不到路由参数layout.tsx不随子路由重渲染改用template.tsx或在页面中获取跳转后页面不刷新从next/router导入了useRouter改为import { useRouter } from next/navigation刷新并行路由页面 404缺少默认插槽 UI增加default.tsx构建报路由重复pages和app下存在相同 URL只保留一套路由定义客户端组件里用了 fetch 缓存use client组件里的 fetch 不做 Next 缓存改用 SWR、React Query 或服务端组件包裹useSearchParams导致 build 警告需要 Suspense 包裹在组件外层增加Suspense边界5. 实际项目怎么选渐进式迁移路线与场景判断5.1 老项目如何渐进式迁到 App Router很多人以为从 Pages Router 迁到 App Router 必须一次性推倒重来实际上 Next.js 官方允许两套路由并存迁移可以按模块渐进完成。我在某内容后台项目上就是这么做的整体节奏分四步走。第一步升级依赖版本先把 Next.js 升到支持 App Router 稳定版本的某个版本确认原有 Pages Router 的路由和页面不受影响。第二步挑一个访问量低、结构清晰的子模块做试点比如/docs、/about把它迁移到app目录下同时保留pages下其它页面。这一步主要是让团队成员熟悉layout.tsx、loading.tsx、error.tsx这些新概念跑通之后再扩大范围。第三步把_app.js里的全局 Provider、全局样式逐步迁移到根layout.tsx同时把pages/api下的接口搬到app/api的 Route Handler。第四步等所有页面都迁完确认没有路由冲突再删掉整个pages目录。这个流程里最需要花心思的是第三步尤其是全局入口。_app.js里可能堆了很多东西全局状态、鉴权逻辑、主题切换、错误上报这其中的一部分逻辑在 App Router 里需要放到根布局或 middleware 里处理需要逐项确认。另外要注意pages和app并存只是过渡方案不要想着长期混用。我曾经在一个项目里同时维护两套路由遇到跳转方向不一致、组件复用混乱、团队协作时互相影响一堆问题。最后统一迁到 App Router 后结构反而清爽很多。5.2 哪些项目其实没必要急着动不是所有项目都适合马上全量切 App Router有些情况保持 Pages Router 反而更稳。如果项目是运营多年的线上业务页面量大、接口逻辑复杂、又缺少自动化测试全量迁移的风险会比较高可以先在小模块试点让团队积累信心再决定要不要继续推。如果项目大量依赖getInitialProps或自定义_document.js迁移成本也会显著提高因为这些 API 在 App Router 里没有对应的、等价的实现思路基本都要重写。还有一类项目大量使用客户端状态管理和复杂第三方库服务端组件的收益不明显迁移要优先控制范围。反过来新项目、内容型站点、需要复杂嵌套布局的中后台、对首屏性能和 SEO 有要求的站点这些场景直接上 App Router 基本没悬念。尤其是从零开始的项目一开始就用 App Router可以避免将来二次迁移的重复成本。我个人实际操作下来的体会是App Router 确实代表了 Next.js 未来的方向但它不是灵丹妙药。真正让迁移价值最大化的是先想清楚项目的核心诉求。最后再分享一个小技巧无论选哪套路由先把一个不重要的子模块完整跑一遍把 layout、loading、error、并行路由、数据缓存这些机制都真实用一次再评估整体迁移工作量。亲测这种方式比对着文档猜靠谱得多。