恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Next.js 从 Pages Router 完整迁移到 App Router:以博客应用的迁移任务为实战范本

  • 首页
  • 资讯中心
  • /
  • Next.js 从 Pages Router 完整迁移到 App Router:以博客应用的迁移任务为实战范本

相关资讯

Headroom 文件系统契约:双根目录模型、环境变量优先级与 Docker/插件路径设计 2026/9/7 17:20:04
数控车床程序编制实战:阶台轴从图纸到上机全流程解析 2026/9/7 17:20:04
从“特效差不多”到上线达标:Canvas粒子特效性能优化与落地指南 2026/9/7 17:20:04

最新资讯

Transformers 文档导航深度解析:德语文档索引中的五段式文档结构、模型目录与框架兼容矩阵
GPT-Academic 二级菜单插件开发指南:从 GptAcademicPluginTemplate 模板到前后端交互原理
Zed Agent 评测夹具深潜:Zode 提示词如何定义一个非交互式代码 Agent,以及评测管线如何消费它
Buzz 离线转录工具:本地语音转文字免费用,三步完成首次转录
决策树进阶:从CART剪枝到连续值与缺失值处理全解析
Python文本相似度计算系统:算法、优化与应用

今日推荐

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现
UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南
BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Next.js 从 Pages Router 完整迁移到 App Router:以博客应用的迁移任务为实战范本

发布时间:2026/9/7 17:20:04
Next.js 从 Pages Router 完整迁移到 App Router:以博客应用的迁移任务为实战范本 Next.js 从 Pages Router 完整迁移到 App Router以博客应用的迁移任务为实战范本【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文以 Next.js 仓库中evals/evals/agent-030-app-router-migration-hard/PROMPT.md定义的迁移任务为核心围绕它所要求的全部约束——迁移所有路由与文件、彻底删除pages目录、替换已废弃的 Pages Router API、补齐 TypeScript 类型——逐一展开。配合该评测目录下自带的 Pages Router 博客项目含getServerSideProps、getStaticProps ISR、getStaticPathsfallback、API Routes、_app/_document等高级模式及其验收测试 EVAL.ts你将掌握每一种 Pages Router API 在 App Router 中的标准替代写法以及如何用自动化测试验证迁移的正确性。一、任务要求一份“Hard”难度的迁移任务书在约束什么迁移任务的原始定义只有一段话但信息量很密集见 PROMPT.md将 Pages Router 中的每一条路由和每一个文件迁移到 App Router。完成后彻底删除 pages 目录。确保使用了正确的 App Router API。如果某个 Pages Router API 在 App Router 中已不存在请替换为新版 API 或新模式。记得添加类型。拆开来看它提出了五层递进的约束全量迁移不是迁移部分页面而是“every route and file”包括隐藏文件_app.js、_document.js、_error.js和 API 路由彻底删除pages目录不允许两套 Router 共存混用迁移完成后项目中不能残留任何 Pages Router 入口API 语义对齐next/head、next/router、getInitialProps、req/res风格的 API handler 等 Pages Router 专属 API 必须换成 App Router 对应物新模式替换部分 Pages Router 能力如getServerSideProps返回props在 App Router 中没有同名 API要改写为“async Server Component 内直接取数据”这类新模式类型化所有产物必须是.ts/.tsx函数签名、params、children等都要有类型——这正是目录名 “hard” 的难度来源它不是简单改文件名而是要求同时处理数据获取、路由处理器、Metadata API、use client指令放置等多个进阶模式的迁移。评测环境的工程配置也值得先看一眼。package.json 声明了next: ^16、react: 19.1.0、typescript: ^5脚本为标准的dev/build/start即next dev、next build、next startnext.config.ts 是空的NextConfig意味着迁移不需要任何额外配置项tsconfig.json 开启了strict: true并配置了/*路径别名验收时类型检查是真实生效的。二、原始 Pages Router 项目盘点每个文件对应什么 App Router 模式迁移前的项目是一个典型的复杂博客应用文件结构如下evals/evals/agent-030-app-router-migration-hard/ ├── components/AppProvider.js # Context Provider主题状态 ├── pages/ │ ├── _app.js # 全局包装导航 AppProvider 全局样式 │ ├── _document.js # 自定义 html/bodylang、favicon、字体 preconnect │ ├── _error.js # 自定义错误页getInitialProps │ ├── 404.js # 自定义 404 │ ├── index.js # 首页getServerSideProps Head useRouter │ ├── blog/ │ │ ├── index.js # 列表getStaticProps revalidate: 60 │ │ └── [id].js # 详情getStaticPaths fallback:blocking │ └── api/posts/ │ ├── index.js # GET/POST 文章 │ └── [id].js # GET/PUT/DELETE 单篇文章 └── styles/globals.css按任务要求逐文件映射到 App Router对照关系是Pages Router 文件/APIApp Router 替代pages/_app.jspages/_document.jsapp/layout.tsx根布局含html/body/metadata/childrenpages/_error.jsgetInitialPropsapp/error.tsxuse client错误边界pages/404.jsapp/not-found.tsxpages/index.js的getServerSidePropsapp/page.tsxasync Server Component 内直接fetchpages/blog/index.js的getStaticPropsrevalidateapp/blog/page.tsxexport const revalidatepages/blog/[id].js的getStaticPathsfallbackapp/blog/[id]/page.tsxgenerateStaticParamspages/api/posts/index.jsapp/api/posts/route.tsGET/POST函数pages/api/posts/[id].jsapp/api/posts/[id]/route.tsGET/PUT/DELETE函数next/head的Headexport const metadatanext/router的useRouternext/navigation的useRouter仅客户端组件components/AppProvider.jsJS保留并迁移为 TS 客户端组件在根布局中包裹children验收测试 EVAL.ts 的 7 个用例恰好逐条覆盖这张映射表下一节按映射表逐项展开。三、_app.js_document.js→ 根布局app/layout.tsx原始的两个隐藏文件分工是_app.js 在每次路由切换时包裹页面提供导航头、main容器、页脚和 AppProvider一个持有theme状态、基于createContext/useState的 Context Provider还带useAppContext守卫_document.js 则声明Html langen、favicon 和 Google Fonts 的preconnect 样式表。App Router 中没有这两个文件它们合并为唯一的根布局app/layout.tsx。验收测试的第一个用例EVAL.ts 中 “Root layout exists and replaces _app/_document”用正则锁定了四条硬性断言html标签必须带lang属性承接_document.js的langen必须有body标签文件内容必须出现metadata/Metadata承接_document.js中Head的description等站点级 meta;必须接收children且类型为ReactNode替代_app.js的Component {...pageProps} /。因此迁移后的根布局骨架应类似// app/layout.tsx import type { Metadata } from next import type { ReactNode } from react import ./globals.css import { AppProvider } from /components/AppProvider export const metadata: Metadata { description: A complex blog application, icons: { icon: /favicon.ico }, } export default function RootLayout({ children, }: { children: ReactNode }) { return ( html langen head link relpreconnect hrefhttps://fonts.googleapis.com / link hrefhttps://fonts.googleapis.com/css2?familyInter:wght400;500;600displayswap relstylesheet / /head body AppProvider{children}/AppProvider /body /html ) }两个细节需要注意原_app.js里的导航头/页脚等静态 UI 可以直接写进布局布局是 Server Componentnext/link在其中可用而AppProvider使用了useState属于客户端逻辑因此迁移为 TS 后组件本身需要加use client再在布局中包裹children——这正是任务书“properuse clientdirective placement”所指的内容之一站点级description从_document.js的meta提升为布局的metadata导出favicon 用icons字段声明字体preconnect直接放在head中测试只校验html lang、body、metadata与children: ReactNode字体链接属于可自由保留的细节。四、getServerSideProps→ async Server Component 直接取数首页 pages/index.js 是整套样例中最典型的 SSR 页面原实现有三块// pages/index.js迁移前 export async function getServerSideProps({ req }) { const userAgent req.headers[user-agent] || const posts await fetch( https://jsonplaceholder.typicode.com/posts?_limit5 ).then((res) res.json()) return { props: { posts, userAgent, timestamp: new Date().toISOString() }, } }req.headers[user-agent]Pages Router 直接暴露 Node 风格的req对象fetch文章列表并在服务端渲染时打时间戳组件内混用了Headnext/head和next/router的useRouter点击按钮router.push(/blog)。App Router 中等价的做法是把页面直接写成 async Server Component在渲染期间执行fetch请求头改用next/headers的headers()API 读取req对象在 App Router 的页面中不存在了// app/page.tsx import { headers } from next/headers import type { Metadata } from next import { HomeClient } from ./home-client export const metadata: Metadata { title: Home - My Blog, description: Welcome to my blog homepage, openGraph: { title: Home - My Blog }, } export default async function HomePage() { const h await headers() const userAgent h.get(user-agent) || const posts await fetch( https://jsonplaceholder.typicode.com/posts?_limit5 ).then((res) res.json()) return ( main pServer-side rendered at: {new Date().toISOString()}/p pYour user agent: {userAgent}/p {/* 渲染 posts 列表 */} HomeClient / /main ) }这里有三处关键转换req→headers()Server Component 中不再有请求/响应对象读请求头一律走next/headersHead→metadata导出title、description、og:title分别落到title、description和openGraph.title字段客户端交互拆分带onClick的按钮不能留在 Server Component 里需要拆出客户端组件测试文件检查的是app/home-client.tsx。在该客户端组件里useRouter必须改从next/navigation导入而不是next/router——EVAL.ts 的 “Client components use next/navigation hooks” 用例会同时断言import.*useRouter.*next\/navigation必须存在、next/router的导入必须不存在。另外注意该用例对首页的判定使用了 LLM 语义评审environment.toSatisfyCriterion其判分说明写得很明确——“Judge runtime behavior, not style: any organization that renders the fetched data from a Server Component is correct”。也就是说数据获取可以内联fetch也可以抽到 helper 函数组件不强制export default async function这种确切形态。这提示我们迁移时不必逐行复刻旧结构只要保证“Server Component 在服务端渲染期取到并渲染数据”这一运行时语义即可。五、getStaticPropsrevalidate→ 顶层revalidateISR博客列表页 pages/blog/index.js 使用getStaticProps抓取全部文章并通过返回值中的revalidate: 60实现 60 秒的 ISR。App Router 中getStaticProps不存在等价物是两件事的组合// app/blog/page.tsx import type { Metadata } from next export const metadata: Metadata { title: Blog - My Blog, description: All blog posts, } export const revalidate 60 // 与 getStaticProps 的 revalidate: 60 等价 export default async function BlogIndex() { const posts await fetch(https://jsonplaceholder.typicode.com/posts).then( (res) res.json() ) return ( div h1All Blog Posts/h1 {/* 渲染 posts 卡片“Read More” 的跳转逻辑同样应拆到客户端组件 */} /div ) }对应关系是return { props, revalidate: 60 }中的revalidate从函数返回值提升为模块顶层的export const revalidateprops则不再需要——页面自己就是 async 函数取到什么就渲染什么。测试 “Blog index migrated with ISR equivalent” 对此有三条断言页面必须是 async Server Component、内容必须出现revalidate且后跟数字/revalidate.*\d/、剥离注释后的代码中不允许残留getStaticProps字样。最后这条断言依赖一个值得留意的工具函数stripCommentsEVAL.ts 第 19-21 行它先删块注释再删行注释然后才做正则匹配。换句话说迁移后的文件里允许用注释说明“这里原来是什么 API”迁移备注不会被判违规但实际执行代码里必须彻底移除旧 API。这是一个很实用的迁移习惯注释可以记录来路代码只能去新路。六、getStaticPathsfallback: blocking→generateStaticParams动态详情页 pages/blog/[id].js 是样例中最复杂的路由同时涉及三种机制getStaticPaths预取前 10 篇文章生成paths并声明fallback: blocking源文件第 4-17 行getStaticProps并行fetch文章正文与评论Promise.allrevalidate: 300抓取失败时返回notFound: true组件中用router.isFallback显示Loading...。迁移到app/blog/[id]/page.tsx后的对应模式// app/blog/[id]/page.tsx import type { Metadata } from next import { notFound } from next/navigation import { BlogPostClient } from ./blog-post-client export const revalidate 300 // 对应 getStaticProps 的 revalidate: 300 export async function generateStaticParams() { const posts await fetch(https://jsonplaceholder.typicode.com/posts).then( (res) res.json() ) return posts.slice(0, 10).map((post) ({ id: post.id.toString(), })) } export async function generateMetadata({ params, }: { params: Promise{ id: string } }): PromiseMetadata { const { id } await params const post await fetch( https://jsonplaceholder.typicode.com/posts/${id} ).then((res) res.json()) return { title: ${post.title} - My Blog, description: post.body.substring(0, 160), openGraph: { title: post.title, description: post.body.substring(0, 160), }, } } export default async function BlogPost({ params, }: { params: Promise{ id: string } }) { const { id } await params let post: { title: string; body: string } let comments: { id: number; name: string; body: string; email: string }[] try { ;[post, comments] await Promise.all([ fetch(https://jsonplaceholder.typicode.com/posts/${id}).then((r) r.json()), fetch(https://jsonplaceholder.typicode.com/posts/${id}/comments).then( (r) r.json() ), ]) } catch { notFound() // 对应 getStaticProps 返回 notFound: true } return ( article BlogPostClient id{id} / h1{post.title}/h1 p{post.body}/p {/* 渲染 comments 列表 */} /article ) }逐点对照getStaticPaths→generateStaticParams返回值形状一致[{ params }]函数体内同样先fetch再slice(0, 10).map。测试断言该文件必须导出generateStaticParams且为 async Server Component同时剥离注释后不得出现getStaticPaths或getStaticPropsfallback: blocking的归宿App Router 没有fallback选项构建时未覆盖的路径会按需构建加载态由 React 的Suspense/框架的流式渲染机制承担组件里原来的router.isFallback分支没有直接等价物可以移除或改用 Suspense 骨架屏notFound: true→notFound()动态导入notFound()并调用即可触发全局not-found.tsxparams是 Promisenext ^16中路由的params为异步对象页面与generateMetadata都需要await params——这正是任务书 “Make sure to add types” 里最容易踩坑的类型点{ params: Promise{ id: string } }。七、API Routes → Route Handlers两条 API 路由是典型的req/res风格pages/api/posts/index.jsGET返回模拟文章列表POST校验title/content后创建文章400/201其他方法返回 405 并设置Allow头pages/api/posts/[id].jsGET单篇、PUT更新、DELETE删除同样带 405 Allow头兜底。App Router 的对应物是route.ts文件导出与 HTTP 方法同名的异步函数使用标准的Request/Response或NextRequest/NextResponse// app/api/posts/route.ts import { NextRequest, NextResponse } from next/server export async function GET() { const posts [ { id: 1, title: First Post, content: This is the first post }, { id: 2, title: Second Post, content: This is the second post }, ] return NextResponse.json(posts) } export async function POST(request: NextRequest) { const { title, content } await request.json() if (!title || !content) { return NextResponse.json( { error: Title and content are required }, { status: 400 } ) } const newPost { id: Date.now(), title, content, createdAt: new Date().toISOString(), } return NextResponse.json(newPost, { status: 201 }) }动态段则改为从路由参数取值——Pages Router 里从req.query拿idRoute Handler 里改为context.params异步对象需await// app/api/posts/[id]/route.ts import { NextRequest, NextResponse } from next/server type RouteContext { params: Promise{ id: string } } export async function GET(_request: NextRequest, context: RouteContext) { const { id } await context.params return NextResponse.json({ id: parseInt(id), title: Post ${id}, content: This is the content for post ${id}, createdAt: new Date().toISOString(), }) } // PUT / DELETE 同理await context.params 后按原逻辑返回测试 “API routes migrated to Route Handlers” 的断言是app/api/posts/route.ts必须导出GET/POST且内容中出现Request/Response/NextRequest/NextResponse之一app/api/posts/[id]/route.ts必须导出GET/PUT/DELETE之一。原 handler 里res.status(405)Allow头的兜底逻辑可以整体省略Route Handler 只注册你导出的方法未导出的方法自动得到 405。八、Metadata API 全面替换next/head样例项目中next/head出现了 5 次首页、博客列表、文章详情、_error.js、404.js。迁移后的统一规则是——页面文件导出metadata或 async 的generateMetadata不再 importnext/head静态标题/描述export const metadata: Metadata { title, description, openGraph }依赖数据的标题文章详情页的post.title使用generateMetadata其params同样是Promise类型站点级 meta原_document.js的description、favicon提升到根布局的metadata导出。验收测试 “Metadata API replaces next/head” 对app/page.tsx与app/blog/page.tsx各做了双向断言必须匹配export.*metadata且不允许出现import.*Head.*next/head或Head标签。也就是说旧 API 既不能“导入着用”也不能以 JSX 形式残留。九、错误处理_error.js与404.js→error.tsxnot-found.tsx原始错误处理有两个文件pages/_error.js接收statusCode区分 404 与服务端/客户端错误文案并通过getInitialProps从res.statusCode/err.statusCode推断状态码pages/404.js静态 404 文案加“回到首页”按钮又用了一次next/router。App Router 的对应物// app/error.tsx use client // 错误边界必须是客户端组件 export default function Error({ error, reset, }: { error: Error { digest?: string } reset: () void }) { return ( div classNameerror-page h1Sorry, something went wrong./h1 button onClick{reset}Try again/button /div ) }// app/not-found.tsx import Link from next/link export default function NotFound() { return ( div classNameerror-page h1404 - Page Not Found/h1 pThe page youapos;re looking for doesnapos;re exist./p Link href/Go Back Home/Link /div ) }对照要点error.tsx的errorprop 承接了_error.js的errstatusCode不再显式传递404 场景由not-found.tsx分流处理reset提供了原实现没有的“重试”能力not-found.tsx中的“回到首页”按钮改用next/link或服务端可渲染的跳转避免在纯展示页面依赖next/navigation。测试 “Error handling migrated to error.js and not-found.js” 的断言是app/error.tsx必须带use client且出现error与Error的匹配即接收 error propapp/not-found.tsx必须存在。另外_error.js的getInitialProps写法整体删除——App Router 没有页面级getInitialProps。十、类型化与验收清单EVAL.ts 是迁移完成的“定义”任务书最后一句 “Make sure to add types” 在这个项目里落到了几处具体的类型签名上它们也恰好是迁移中最容易写错的地方位置需要的类型app/layout.tsx{ children: ReactNode }children必须声明为ReactNode测试用/children.*ReactNode/校验动态页面{ params: Promise{ id: string } }且渲染前await paramsgenerateMetadata返回PromiseMetadataRoute HandlerNextRequest/NextResponse参数与RouteContext类型AppProvider迁移为 TS 时给createContext补上{ theme: string; setTheme: React.DispatchReact.SetStateActionstring }类型并加use client最终EVAL.ts 提供了完整的自动化验收清单7 个用例逐一锁定迁移的“完成”定义测试用例核心验证点Root layout exists and replaces _app/_documentapp/layout.tsx存在html lang、body、metadata、children: ReactNodeHome page migrated to Server Component with async data fetchingapp/page.tsx存在由 LLM 评审确认是“渲染服务端取回数据的 async Server Component”不看代码风格看运行时行为Blog index migrated with ISR equivalentasync 页面 revalidate数字 代码中无getStaticPropsDynamic blog route migrated to generateStaticParams导出generateStaticParams async 代码中无getStaticPaths/getStaticPropsAPI routes migrated to Route Handlers导出 HTTP 方法函数 使用 Request/Response 系 APIMetadata API replaces next/head导出metadata且无next/head导入或HeadError handling migrated to error.js and not-found.jserror.tsx带use client且接收errorpropnot-found.tsx存在Client components use next/navigation hooksuseRouter必须来自next/navigation禁止next/router这套测试设计本身也有两点值得借鉴其一stripComments让“迁移备注注释”与“真实代码残留”被区别对待正则检查只针对实际执行的代码其二对存在多种合法形态的语义性检查首页的 Server Component 取数没有硬编码正则而是交给 LLM 评审并给评审附上了一份“参考正确形态”——这避免了旧版正则误杀把fetch抽到 helper 的正确解法。结语这个 “hard” 迁移任务的全部复杂度浓缩起来就是三张映射表Pages 专属 APIgetServerSideProps/getStaticProps/getStaticPaths/getInitialProps/req.reshandler到 App Router 模式async Server Component、顶层revalidate、generateStaticParams、Route Handler的替换next/head与next/router到metadata/next/navigation的导入迁移以及 JS 文件到带类型.ts/.tsx产物的转换params是Promise、children是ReactNode。按照 PROMPT.md 的要求完成后pages目录应当被整体移除项目只保留app目录与客户端组件并以 EVAL.ts 的 7 个用例作为迁移正确性的可执行定义。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号