跳到内容
构建你的应用升级从 Pages 到 App

从 Pages 到 App

本指南将帮助你

升级

Node.js 版本

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

Next.js 版本

要更新到 Next.js 版本 13,请使用你偏好的包管理器运行以下命令

终端
npm install next@latest react@latest react-dom@latest

ESLint 版本

如果你正在使用 ESLint,你需要升级你的 ESLint 版本

终端
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 Router,其中包含新功能和约定。新的 Router 在 app 目录中可用,并与 pages 目录共存。

升级到 Next.js 13 需要使用 App Router。你可以继续使用 pages,并使用在两个目录中都适用的新功能,例如更新的 Image 组件Link 组件Script 组件字体优化

<Image/> 组件

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

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

有两个 codemod 可以帮助你迁移到新的 Image 组件

  • next-image-to-legacy-image codemod:安全且自动地将 next/image 导入重命名为 next/legacy/image。现有组件将保持相同的行为。
  • next-image-experimental codemod:危险地添加内联样式并删除未使用的 props。这将更改现有组件的行为以匹配新的默认值。要使用此 codemod,你需要首先运行 next-image-to-legacy-image codemod。

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

例如

import Link from 'next/link'
 
// Next.js 12: `<a>` has to be nested otherwise it's excluded
<Link href="/about">
  <a>About</a>
</Link>
 
// Next.js 13: `<Link>` always renders `<a>` under the hood
<Link href="/about">
  About
</Link>

要将你的链接升级到 Next.js 13,你可以使用 new-link codemod

<Script> 组件

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

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

字体优化

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

虽然内联 CSS 仍然在 pages 中有效,但在 app 中无效。你应该改用 next/font

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

pages 迁移到 app

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

迁移到 App Router 可能是你第一次使用 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({
  // Layouts must accept a children prop.
  // This will be populated with nested layouts or pages
  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: 'Home',
  description: 'Welcome to Next.js',
}

迁移 _document.js_app.js

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

如果你正在使用任何 React Context providers,它们将需要移动到客户端组件

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

Next.js 建议向 Page 组件添加属性,以在 pages 目录中实现每页布局。此模式可以用 app 目录中对嵌套布局的原生支持来替换。

查看之前和之后的示例

之前

components/DashboardLayout.js
export default function DashboardLayout({ children }) {
  return (
    <div>
      <h2>My Dashboard</h2>
      {children}
    </div>
  )
}
pages/dashboard/index.js
import DashboardLayout from '../components/DashboardLayout'
 
export default function Page() {
  return <p>My Page</p>
}
 
Page.getLayout = function getLayout(page) {
  return <DashboardLayout>{page}</DashboardLayout>
}

之后

  • pages/dashboard/index.js 中删除 Page.getLayout 属性,并按照迁移 Pages 的步骤操作,将其迁移到 app 目录。

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

    app/dashboard/DashboardLayout.js
    'use client' // this directive should be at top of the file, before any imports.
     
    // This is a Client Component
    export default function DashboardLayout({ children }) {
      return (
        <div>
          <h2>My Dashboard</h2>
          {children}
        </div>
      )
    }
  • DashboardLayout 导入到 app 目录内新的 layout.js 文件中。

    app/dashboard/layout.js
    import DashboardLayout from './DashboardLayout'
     
    // This is a Server Component
    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>My page title</title>
      </Head>
    </>
  )
}

之后

app/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'My Page Title',
}
 
export default function Page() {
  return '...'
}

查看所有元数据选项.

步骤 4:迁移 Pages

  • 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' 指令添加到文件顶部(在任何导入之前)。
    • 与 Pages Router 类似,有一个优化步骤可以在初始页面加载时将客户端组件预渲染为静态 HTML。
  • 将默认导出的页面组件从 pages/index.js 移动到 app/home-page.tsx
app/home-page.tsx
'use client'
 
// This is a Client Component (same as components in the `pages` directory)
// It receives data as props, has access to state and effects, and is
// prerendered on the server during the initial page load.
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 your Client Component
    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() {
      // Fetch data directly in a Server Component
      const recentPosts = await getPosts()
      // Forward fetched data to your Client Component
      return <HomePage recentPosts={recentPosts} />
    }
  • 如果你之前的页面使用了 useRouter,你需要更新到新的路由 Hooks。了解更多

  • 启动你的开发服务器,访问 https://127.0.0.1:3000。你应该能看到你现有的 index 路由,现在通过 app 目录提供服务。

步骤 5:迁移路由钩子

一个新的路由器已被添加到 app 目录中,以支持新的行为。

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

  • 新的 useRouter 钩子是从 next/navigation 导入的,并且与从 next/router 导入的 pages 中的 useRouter 钩子具有不同的行为。
    • next/router 导入的 useRouter 钩子app 目录中不受支持,但可以继续在 pages 目录中使用。
  • 新的 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() {
  // This request should be cached until manually invalidated.
  // Similar to `getStaticProps`.
  // `force-cache` is the default and can be omitted.
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })
 
  // This request should be refetched on every request.
  // Similar to `getServerSideProps`.
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
 
  // This request should be cached with a lifetime of 10 seconds.
  // Similar to `getStaticProps` with the `revalidate` option.
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })
 
  return <div>...</div>
}

服务器端渲染 (getServerSideProps)

pages 目录中,getServerSideProps 用于在服务器上获取数据,并将 props 转发到文件中默认导出的 React 组件。页面的初始 HTML 是从服务器预渲染的,然后是浏览器中的页面“hydration”(使其具有交互性)。

pages/dashboard.js
// `pages` directory
 
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` directory
 
// This function can be named anything
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` directory
 
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` directory
import { cookies, headers } from 'next/headers'
 
async function getData() {
  const authHeader = (await headers()).get('authorization')
 
  return '...'
}
 
export default async function Page() {
  // You can use `cookies` or `headers` inside Server Components
  // directly or in your data fetching function
  const theme = (await cookies()).get('theme')
  const data = await getData()
  return '...'
}

静态站点生成 (getStaticProps)

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

pages/index.js
// `pages` directory
 
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` directory
 
// This function can be named anything
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` directory
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` directory
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} />
}

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


替换 fallback

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

pages/posts/[id].js
// `pages` directory
 
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` directory
 
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(默认值)时,如果请求了尚未生成的路由段,它将被服务器渲染并缓存。

增量静态再生 (getStaticPropsrevalidate)

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

pages/index.js
// `pages` directory
 
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` directory
 
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}', // <-- Add this line
    './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 Router 与 Pages Router 一起使用

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

相反,你可以优化导航 在 App Router 和 Pages Router 之间,以保留预取和快速页面转换。了解更多

Codemods

Next.js 提供了 Codemod 转换,以帮助在功能被弃用时升级你的代码库。有关更多信息,请参阅 Codemods