跳到主要内容

如何从 Pages 路由器迁移到 App 路由器

本指南将帮助您:

升级

Node.js 版本

最低 Node.js 版本现在是 v18.17。有关更多信息,请参阅 Node.js 文档

Next.js 版本

要更新到 Next.js 版本 13,请使用您首选的包管理器运行以下命令:

Terminal
npm install next@latest react@latest react-dom@latest

ESLint 版本

如果您使用 ESLint,您需要升级 ESLint 版本:

Terminal
npm install -D eslint-config-next@latest

提示: 您可能需要重启 VS Code 中的 ESLint 服务器以使 ESLint 更改生效。打开命令面板(Mac 上为 cmd+shift+p;Windows 上为 ctrl+shift+p)并搜索 ESLint: Restart ESLint Server

下一步

更新后,请参阅以下部分了解下一步:

升级新功能

Next.js 13 引入了新的 App 路由器,具有新功能和约定。新路由器在 app 目录中可用,与 pages 目录共存。

升级到 Next.js 13 不需要使用 App 路由器。您可以继续使用 pages 以及适用于两个目录的新功能,如更新的 Image 组件Link 组件Script 组件字体优化

<Image/> 组件

Next.js 12 引入了 Image 组件的新改进,使用临时导入:next/future/image。这些改进包括更少的客户端 JavaScript、更容易扩展和样式化图片的方法、更好的可访问性和原生浏览器懒加载。

在版本 13 中,这种新行为现在是 next/image 的默认行为。

有两个代码修改工具可以帮助您迁移到新的 Image 组件:

<Link> 组件 不再需要手动添加 <a> 标签作为子元素。此行为在 版本 12.2 中作为实验性选项添加,现在是默认行为。在 Next.js 13 中,<Link> 总是渲染 <a> 并允许您将 props 转发到底层标签。

例如:

import Link from 'next/link'

// Next.js 12:`<a>` 必须嵌套,否则会被排除
<Link href="/about">
<a>关于</a>
</Link>

// Next.js 13:`<Link>` 总是在底层渲染 `<a>`
<Link href="/about">
关于
</Link>

要将链接升级到 Next.js 13,您可以使用 new-link 代码修改工具

<Script> 组件

next/script 的行为已更新以支持 pagesapp,但需要进行一些更改以确保平滑迁移:

  • 将您之前在 _document.js 中包含的任何 beforeInteractive 脚本移动到根布局文件(app/layout.tsx)。
  • 实验性 worker 策略在 app 中还不能工作,使用此策略的脚本将必须被删除或修改为使用不同的策略(例如 lazyOnload)。
  • onLoadonReadyonError 处理程序在服务器组件中不能工作,所以请确保将它们移动到客户端组件或完全删除它们。

字体优化

以前,Next.js 通过内联字体 CSS 帮助您优化字体。版本 13 引入了新的 next/font 模块,它让您能够自定义字体加载体验,同时仍然确保出色的性能和隐私。next/fontpagesapp 目录中都受支持。

虽然内联 CSSpages 中仍然有效,但在 app 中不起作用。您应该使用 next/font 代替。

请参阅字体优化页面以了解如何使用 next/font

pages 迁移到 app

🎥 观看: 了解如何逐步采用 App 路由器 → YouTube(16 分钟)

迁移到 App 路由器可能是第一次使用 Next.js 构建的 React 功能,如服务器组件、Suspense 等。当与新的 Next.js 功能(如特殊文件布局)结合时,迁移意味着需要学习新概念、心智模型和行为变化。

我们建议通过将迁移分解为更小的步骤来减少这些更新的组合复杂性。app 目录被有意设计为与 pages 目录同时工作,以允许逐页增量迁移。

  • app 目录支持嵌套路由 布局。了解更多
  • 使用嵌套文件夹定义路由,使用特殊的 page.js 文件使路由段公开可访问。了解更多
  • 特殊文件约定用于为每个路由段创建 UI。最常见的特殊文件是 page.jslayout.js
    • 使用 page.js 定义路由特有的 UI。
    • 使用 layout.js 定义在多个路由之间共享的 UI。
    • 特殊文件可以使用 .js.jsx.tsx 文件扩展名。
  • 您可以在 app 目录中并置其他文件,如组件、样式、测试等。了解更多
  • 数据获取函数如 getServerSidePropsgetStaticProps 已被 app 内的新 API 替换。getStaticPaths 已被 generateStaticParams 替换。
  • pages/_app.jspages/_document.js 已被单个 app/layout.js 根布局替换。了解更多
  • pages/_error.js 已被更细粒度的 error.js 特殊文件替换。了解更多
  • pages/404.js 已被 not-found.js 文件替换。
  • pages/api/* API 路由已被 route.js(路由处理程序)特殊文件替换。

步骤 1:创建 app 目录

更新到最新的 Next.js 版本(需要 13.4 或更高版本):

npm install next@latest

然后,在项目根目录(或 src/ 目录)创建一个新的 app 目录。

步骤 2:创建根布局

app 目录内创建一个新的 app/layout.tsx 文件。这是一个根布局,将应用于 app 内的所有路由。

app/layout.tsx
export default function RootLayout({
// 布局必须接受 children prop。
// 这将用嵌套布局或页面填充
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
  • app 目录必须包含根布局。
  • 根布局必须定义 <html><body> 标签,因为 Next.js 不会自动创建它们
  • 根布局替换 pages/_app.tsxpages/_document.tsx 文件。
  • 布局文件可以使用 .js.jsx.tsx 扩展名。

要管理 <head> HTML 元素,您可以使用内置 SEO 支持

app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: '首页',
description: '欢迎使用 Next.js',
}

迁移 _document.js_app.js

如果您有现有的 _app_document 文件,可以将内容(例如全局样式)复制到根布局(app/layout.tsx)。app/layout.tsx 中的样式将_不_应用于 pages/*。在迁移过程中,您应该保留 _app/_document 以防止 pages/* 路由中断。完全迁移后,您可以安全地删除它们。

如果您使用任何 React Context 提供者,它们需要移动到客户端组件

getLayout() 模式迁移到布局(可选)

Next.js 建议在 pages 目录中为页面组件添加属性以实现每页布局。这种模式可以用 app 目录中嵌套布局的原生支持替换。

查看前后示例

之前

components/DashboardLayout.js
export default function DashboardLayout({ children }) {
return (
<div>
<h2>我的仪表板</h2>
{children}
</div>
)
}
pages/dashboard/index.js
import DashboardLayout from '../components/DashboardLayout'

export default function Page() {
return <p>我的页面</p>
}

Page.getLayout = function getLayout(page) {
return <DashboardLayout>{page}</DashboardLayout>
}

之后

  • pages/dashboard/index.js 中删除 Page.getLayout 属性,并按照迁移页面的步骤app 目录。

    app/dashboard/page.js
    export default function Page() {
    return <p>我的页面</p>
    }
  • DashboardLayout 的内容移动到新的客户端组件以保留 pages 目录行为。

    app/dashboard/DashboardLayout.js
    'use client' // 此指令应在文件顶部,在任何导入之前。

    // 这是一个客户端组件
    export default function DashboardLayout({ children }) {
    return (
    <div>
    <h2>我的仪表板</h2>
    {children}
    </div>
    )
    }
  • DashboardLayout 导入到 app 目录内的新 layout.js 文件中。

    app/dashboard/layout.js
    import DashboardLayout from './DashboardLayout'

    // 这是一个服务器组件
    export default function Layout({ children }) {
    return <DashboardLayout>{children}</DashboardLayout>
    }
  • 您可以逐步将 DashboardLayout.js(客户端组件)的非交互部分移动到 layout.js(服务器组件)中,以减少发送到客户端的组件 JavaScript 量。

步骤 3:迁移 next/head

pages 目录中,next/head React 组件用于管理 <head> HTML 元素,如 titlemeta。在 app 目录中,next/head 被新的内置 SEO 支持替换。

之前:

pages/index.tsx
import Head from 'next/head'

export default function Page() {
return (
<>
<Head>
<title>我的页面标题</title>
</Head>
</>
)
}

之后:

app/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: '我的页面标题',
}

export default function Page() {
return '...'
}

查看所有元数据选项

步骤 4:迁移页面

  • app 目录中的页面默认是服务器组件。这与 pages 目录不同,pages 目录中页面是客户端组件
  • 数据获取app 中已更改。getServerSidePropsgetStaticPropsgetInitialProps 已被更简单的 API 替换。
  • app 目录使用嵌套文件夹定义路由,使用特殊的 page.js 文件使路由段公开可访问。
  • pages 目录app 目录路由
    index.jspage.js/
    about.jsabout/page.js/about
    blog/[slug].jsblog/[slug]/page.js/blog/post-1

我们建议将页面迁移分解为两个主要步骤:

  • 步骤 1:将默认导出的页面组件移动到新的客户端组件。
  • 步骤 2:将新的客户端组件导入到 app 目录内的新 page.js 文件中。

提示: 这是最简单的迁移路径,因为它具有与 pages 目录最可比的行为。

步骤 1:创建新的客户端组件

  • app 目录内创建一个新的单独文件(即 app/home-page.tsx 或类似),导出客户端组件。要定义客户端组件,在文件顶部(在任何导入之前)添加 'use client' 指令。
    • 与页面路由器类似,有一个优化步骤在初始页面加载时将客户端组件预渲染为静态 HTML。
  • 将默认导出的页面组件从 pages/index.js 移动到 app/home-page.tsx
app/home-page.tsx
'use client'

// 这是一个客户端组件(与 `pages` 目录中的组件相同)
// 它接收数据作为 props,可以访问状态和效果,并且
// 在初始页面加载期间在服务器上预渲染。
export default function HomePage({ recentPosts }) {
return (
<div>
{recentPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}

步骤 2:创建新页面

  • app 目录内创建一个新的 app/page.tsx 文件。这默认是一个服务器组件。

  • home-page.tsx 客户端组件导入到页面中。

  • 如果您在 pages/index.js 中获取数据,使用新的数据获取 API 将数据获取逻辑直接移动到服务器组件中。有关更多详细信息,请参阅数据获取升级指南

    app/page.tsx
    // 导入您的客户端组件
    import HomePage from './home-page'

    async function getPosts() {
    const res = await fetch('https://...')
    const posts = await res.json()
    return posts
    }

    export default async function Page() {
    // 在服务器组件中直接获取数据
    const recentPosts = await getPosts()
    // 将获取的数据转发给您的客户端组件
    return <HomePage recentPosts={recentPosts} />
    }
  • 如果您之前的页面使用 useRouter,您需要更新到新的路由钩子。了解更多

  • 启动开发服务器并访问 http://localhost:3000。您应该看到您现有的索引路由,现在通过 app 目录提供服务。

步骤 5:迁移路由钩子

已添加新路由器以支持 app 目录中的新行为。

app 中,您应该使用从 next/navigation 导入的三个新钩子:useRouter()usePathname()useSearchParams()

  • 新的 useRouter 钩子从 next/navigation 导入,与从 next/router 导入的 pages 中的 useRouter 钩子有不同的行为。
  • 新的 useRouter 不返回 pathname 字符串。请改用单独的 usePathname 钩子。
  • 新的 useRouter 不返回 query 对象。搜索参数和动态路由参数现在是分开的。请改用 useSearchParamsuseParams 钩子。
  • 您可以一起使用 useSearchParamsusePathname 来监听页面更改。有关更多详细信息,请参阅路由器事件部分。
  • 这些新钩子仅在客户端组件中受支持。它们不能在服务器组件中使用。
app/example-client-component.tsx
'use client'

import { useRouter, usePathname, useSearchParams } from 'next/navigation'

export default function ExampleClientComponent() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()

// ...
}

此外,新的 useRouter 钩子有以下更改:

  • isFallback 已被删除,因为 fallback 已被替换
  • localelocalesdefaultLocalesdomainLocales 值已被删除,因为内置的 i18n Next.js 功能在 app 目录中不再必要。了解有关 i18n 的更多信息
  • basePath 已被删除。替代方案不会是 useRouter 的一部分。它尚未实现。
  • asPath 已被删除,因为 as 的概念已从新路由器中删除。
  • isReady 已被删除,因为它不再必要。在静态渲染期间,任何使用 useSearchParams() 钩子的组件将跳过预渲染步骤,而是在运行时在客户端渲染。
  • route 已被删除。usePathnameuseSelectedLayoutSegments() 提供替代方案。

查看 useRouter() API 参考

pagesapp 之间共享组件

要保持组件在 pagesapp 路由器之间兼容,请参考 next/compat/router 中的 useRouter 钩子。 这是 pages 目录中的 useRouter 钩子,但旨在在路由器之间共享组件时使用。一旦您准备只在 app 路由器中使用它,请更新到新的next/navigation 中的 useRouter

步骤 6:迁移数据获取方法

pages 目录使用 getServerSidePropsgetStaticProps 为页面获取数据。在 app 目录内,这些以前的数据获取函数被基于 fetch()async React 服务器组件的更简单的 API 替换。

app/page.tsx
export default async function Page() {
// 此请求应被缓存直到手动失效。
// 类似于 `getStaticProps`。
// `force-cache` 是默认值,可以省略。
const staticData = await fetch(`https://...`, { cache: 'force-cache' })

// 此请求应在每个请求上重新获取。
// 类似于 `getServerSideProps`。
const dynamicData = await fetch(`https://...`, { cache: 'no-store' })

// 此请求应缓存 10 秒的生命周期。
// 类似于带有 `revalidate` 选项的 `getStaticProps`。
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 10 },
})

return <div>...</div>
}

服务器端渲染(getServerSideProps

pages 目录中,getServerSideProps 用于在服务器上获取数据并将 props 转发给文件中的默认导出 React 组件。页面的初始 HTML 从服务器预渲染,然后在浏览器中"水合"页面(使其可交互)。

pages/dashboard.js
// `pages` 目录

export async function getServerSideProps() {
const res = await fetch(`https://...`)
const projects = await res.json()

return { props: { projects } }
}

export default function Dashboard({ projects }) {
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}

在 App 路由器中,我们可以使用服务器组件将数据获取并置在我们的 React 组件内。这允许我们向客户端发送更少的 JavaScript,同时保持来自服务器的渲染 HTML。

通过将 cache 选项设置为 no-store,我们可以指示获取的数据应该永远不会被缓存。这类似于 pages 目录中的 getServerSideProps

app/dashboard/page.tsx
// `app` 目录

// 此函数可以命名为任何名称
async function getProjects() {
const res = await fetch(`https://...`, { cache: 'no-store' })
const projects = await res.json()

return projects
}

export default async function Dashboard() {
const projects = await getProjects()

return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}

访问请求对象

pages 目录中,您可以基于 Node.js HTTP API 检索基于请求的数据。

例如,您可以从 getServerSideProps 检索 req 对象,并使用它来检索请求的 cookies 和 headers。

pages/index.js
// `pages` 目录

export async function getServerSideProps({ req, query }) {
const authHeader = req.getHeaders()['authorization'];
const theme = req.cookies['theme'];

return { props: { ... }}
}

export default function Page(props) {
return ...
}

app 目录公开了新的只读函数来检索请求数据:

app/page.tsx
// `app` 目录
import { cookies, headers } from 'next/headers'

async function getData() {
const authHeader = (await headers()).get('authorization')

return '...'
}

export default async function Page() {
// 您可以在服务器组件中直接使用 `cookies` 或 `headers`
// 或在您的数据获取函数中
const theme = (await cookies()).get('theme')
const data = await getData()
return '...'
}

静态站点生成(getStaticProps

pages 目录中,getStaticProps 函数用于在构建时预渲染页面。此函数可用于从外部 API 或直接从数据库获取数据,并在构建期间生成页面时将此数据传递给整个页面。

pages/index.js
// `pages` 目录

export async function getStaticProps() {
const res = await fetch(`https://...`)
const projects = await res.json()

return { props: { projects } }
}

export default function Index({ projects }) {
return projects.map((project) => <div>{project.name}</div>)
}

app 目录中,使用 fetch() 的数据获取将默认为 cache: 'force-cache',这将缓存请求数据直到手动失效。这类似于 pages 目录中的 getStaticProps

app/page.js
// `app` 目录

// 此函数可以命名为任何名称
async function getProjects() {
const res = await fetch(`https://...`)
const projects = await res.json()

return projects
}

export default async function Index() {
const projects = await getProjects()

return projects.map((project) => <div>{project.name}</div>)
}

动态路径(getStaticPaths

pages 目录中,getStaticPaths 函数用于定义应该在构建时预渲染的动态路径。

pages/posts/[id].js
// `pages` 目录
import PostLayout from '@/components/post-layout'

export async function getStaticPaths() {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
}
}

export async function getStaticProps({ params }) {
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()

return { props: { post } }
}

export default function Post({ post }) {
return <PostLayout post={post} />
}

app 目录中,getStaticPathsgenerateStaticParams 替换。

generateStaticParams 的行为类似于 getStaticPaths,但具有简化的 API 用于返回路由参数,可以在布局内使用。generateStaticParams 的返回形状是段数组,而不是嵌套 param 对象数组或解析路径字符串。

app/posts/[id]/page.js
// `app` 目录
import PostLayout from '@/components/post-layout'

export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }]
}

async function getPost(params) {
const res = await fetch(`https://.../posts/${(await params).id}`)
const post = await res.json()

return post
}

export default async function Post({ params }) {
const post = await getPost(params)

return <PostLayout post={post} />
}

使用名称 generateStaticParamsgetStaticPaths 更适合 app 目录中的新模型。get 前缀被更具描述性的 generate 替换,现在 getStaticPropsgetServerSideProps 不再必要,它更适合单独使用。Paths 后缀被 Params 替换,这对于具有多个动态段的嵌套路由更合适。


替换 fallback

pages 目录中,从 getStaticPaths 返回的 fallback 属性用于定义未在构建时预渲染的页面的行为。此属性可以设置为 true 以在页面生成时显示回退页面,false 以显示 404 页面,或 blocking 以在请求时生成页面。

pages/posts/[id].js
// `pages` 目录

export async function getStaticPaths() {
return {
paths: [],
fallback: 'blocking'
};
}

export async function getStaticProps({ params }) {
...
}

export default function Post({ post }) {
return ...
}

app 目录中,config.dynamicParams 属性控制如何处理 generateStaticParams 之外的参数:

  • true:(默认)generateStaticParams 中未包含的动态段按需生成。
  • falsegenerateStaticParams 中未包含的动态段将返回 404。

这替换了 pages 目录中 getStaticPathsfallback: true | false | 'blocking' 选项。fallback: 'blocking' 选项不包含在 dynamicParams 中,因为 'blocking'true 之间的差异在流式传输时微不足道。

app/posts/[id]/page.js
// `app` 目录

export const dynamicParams = true;

export async function generateStaticParams() {
return [...]
}

async function getPost(params) {
...
}

export default async function Post({ params }) {
const post = await getPost(params);

return ...
}

使用 dynamicParams 设置为 true(默认值),当请求尚未生成的路由段时,它将进行服务器渲染并缓存。

增量静态再生成(带有 revalidategetStaticProps

pages 目录中,getStaticProps 函数允许您添加 revalidate 字段以在特定时间后自动重新生成页面。

pages/index.js
// `pages` 目录

export async function getStaticProps() {
const res = await fetch(`https://.../posts`)
const posts = await res.json()

return {
props: { posts },
revalidate: 60,
}
}

export default function Index({ posts }) {
return (
<Layout>
<PostList posts={posts} />
</Layout>
)
}

app 目录中,使用 fetch() 的数据获取可以使用 revalidate,这将缓存指定秒数的请求。

app/page.js
// `app` 目录

async function getPosts() {
const res = await fetch(`https://.../posts`, { next: { revalidate: 60 } })
const data = await res.json()

return data.posts
}

export default async function PostList() {
const posts = await getPosts()

return posts.map((post) => <div>{post.name}</div>)
}

API 路由

API 路由在 pages/api 目录中继续工作,无需任何更改。但是,它们已被 app 目录中的路由处理程序替换。

路由处理程序允许您使用 Web RequestResponse API 为给定路由创建自定义请求处理程序。

app/api/route.ts
export async function GET(request: Request) {}

提示: 如果您之前使用 API 路由从客户端调用外部 API,您现在可以使用服务器组件来安全地获取数据。了解有关数据获取的更多信息。

单页应用程序

如果您同时从单页应用程序(SPA)迁移到 Next.js,请参阅我们的文档以了解更多信息。

步骤 7:样式

pages 目录中,全局样式表仅限于 pages/_app.js。使用 app 目录,此限制已被取消。全局样式可以添加到任何布局、页面或组件。

Tailwind CSS

如果您使用 Tailwind CSS,您需要将 app 目录添加到 tailwind.config.js 文件中:

tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}', // <-- 添加此行
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
}

您还需要在 app/layout.js 文件中导入全局样式:

app/layout.js
import '../styles/globals.css'

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

了解有关使用 Tailwind CSS 样式的更多信息

将 App 路由器与 Pages 路由器一起使用

当在不同 Next.js 路由器提供的路由之间导航时,将会有硬导航。使用 next/link 的自动链接预取不会跨路由器预取。

相反,您可以优化 App 路由器和 Pages 路由器之间的导航以保留预取和快速页面转换。了解更多

代码修改工具

Next.js 提供代码修改转换,以帮助在功能被弃用时升级您的代码库。有关更多信息,请参阅代码修改工具