跳至内容
构建你的应用升级App Router 迁移

App Router 增量采用指南

本指南将帮助你

升级

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: 重新启动 ESLint 服务器

后续步骤

更新后,请参阅以下部分了解后续步骤

升级新功能

Next.js 13 引入了新的 App Router,其中包含新功能和约定。新的路由器可在 app 目录中使用,并与 pages 目录共存。

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

<Image/> 组件

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

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

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

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

<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 代码修改

<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 提供程序,则需要将其移动到客户端组件

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

Next.js 建议向页面组件添加属性以在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属性,并按照迁移页面到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:迁移页面

  • 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'
 
// 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,则需要更新到新的路由钩子。 了解更多

  • 启动您的开发服务器并访问https://127.0.0.1:3000。您应该会看到您现有的索引路由,现在通过 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值已被删除,因为在app目录中不再需要内置的 i18n Next.js 功能。 了解有关 i18n 的更多信息
  • basePath已被删除。替代方案将不会成为useRouter的一部分。它尚未实现。
  • asPath已被删除,因为as的概念已从新路由器中删除。
  • isReady已被删除,因为它不再需要。在静态渲染期间,任何使用useSearchParams()钩子的组件都将跳过预渲染步骤,而是在运行时在客户端进行渲染。
  • route已被删除。usePathnameuseSelectedLayoutSegments()提供了替代方案。

查看useRouter() API 参考.

pagesapp之间共享组件

要使组件在pagesapp路由器之间兼容,请参阅来自next/compat/routeruseRouter钩子。这是来自pages目录的useRouter钩子,但旨在在路由器之间共享组件时使用。一旦您准备好仅在app路由器上使用它,请更新到来自next/navigation的新useRouter

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

pages目录使用getServerSidePropsgetStaticProps来获取页面数据。在app目录中,这些先前的数据获取函数被构建在fetch()和异步 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 从服务器预渲染,然后在浏览器中“水化”页面(使其具有交互性)。

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 Router 中,我们可以使用服务器组件在 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对象,并使用它来检索请求的cookie和标头。

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/${params.id}`)
  const post = await res.json()
 
  return post
}
 
export default async function Post({ params }) {
  const post = await getPost(params)
 
  return <PostLayout post={post} />
}

使用名称generateStaticParams比在app目录中的新模型中使用getStaticPaths更合适。get前缀被替换为更具描述性的generate,现在getStaticPropsgetServerSideProps不再需要,因此它单独存在更好。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中的动态段会按需生成。
  • false:未包含在generateStaticParams中的动态段将返回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,现在可以使用服务器组件来安全地获取数据。了解有关数据获取的更多信息。

步骤 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 进行样式设置的更多信息

代码修改

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