Sponsor
ntab.devntab.dev 提升效率的新标签页组件
点击查看
Menu

App Router 增量采用指南

本指南将帮助你:

升级

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 Router,带有新功能和约定。新的路由器在 app 目录中可用,并与 pages 目录共存。

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

<Image/> 组件

Next.js 12 在临时导入 next/future/image 中引入了 Image 组件的新改进。这些改进包括减少客户端 JavaScript、更简单的扩展和样式图像的方式、更好的可访问性以及原生浏览器懒加载。

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

有两个代码模块可以帮助你迁移到新的 Image 组件:

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

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

例如:

import Link from 'next/link'
 
// Next.js 12:必须嵌套 `<a>`,否则会被排除
<Link href="/about">
  <a>About</a>
</Link>
 
// Next.js 13:`<Link>` 始终在底层渲染 `<a>`
<Link href="/about">
  About
</Link>

要将链接升级到 Next.js 13,你可以使用 new-link 代码模块

<Script> 组件

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

  • 将你之前在 _document.js 中包含的任何 beforeInteractive 脚本移到根布局文件 (app/layout.tsx) 中。
  • 实验性的 worker 策略目前在 app 中不起作用,使用此策略的脚本将必须被移除或修改为使用不同的策略(例如 lazyOnload)。
  • onLoadonReadyonError 处理程序在 Server Components 中不起作用,所以确保将它们移动到 Client Component 中或完全移除它们。

字体优化

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

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

查看 字体优化 页面了解如何使用 next/font

pages 迁移到 app

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

移动到 App Router 可能是你第一次使用 Next.js 构建在其上的 React 功能,如 Server Components、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 (Route Handler) 特殊文件替代。

第 1 步:创建 app 目录

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

npm install next@latest

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

第 2 步:创建根布局

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

app/layout.tsx
TypeScript
export default function RootLayout({
  // 布局必须接受一个 children 属性。
  // 这将被嵌套的布局或页面填充
  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
TypeScript
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 提供者,需要将它们移动到 Client Component

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 的内容移动到一个新的 Client Component,以保留 pages 目录的行为。

    app/dashboard/DashboardLayout.js
    'use client' // 这个指令应该在文件顶部,在任何导入之前。
     
    // 这是一个 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'
     
    // 这是一个 Server Component
    export default function Layout({ children }) {
      return <DashboardLayout>{children}</DashboardLayout>
    }
  • 你可以逐步将 DashboardLayout.js(Client Component)中的非交互部分移动到 layout.js(Server Component)中,以减少发送到客户端的组件 JavaScript 数量。

第 3 步:迁移 next/head

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

之前:

pages/index.tsx
TypeScript
import Head from 'next/head'
 
export default function Page() {
  return (
    <>
      <Head>
        <title>My page title</title>
      </Head>
    </>
  )
}

之后:

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

查看所有元数据选项

第 4 步:迁移页面

  • app 目录 中的页面默认是 Server Components。这与 pages 目录不同,在 pages 目录中,页面是 Client Components
  • 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 步:将默认导出的 Page 组件移动到一个新的 Client Component 中。
  • 第 2 步:将新的 Client Component 导入到 app 目录中的新 page.js 文件中。

值得注意的是:这是最简单的迁移路径,因为它与 pages 目录的行为最相似。

第 1 步:创建一个新的 Client Component

  • app 目录中创建一个新的单独文件(即 app/home-page.tsx 或类似文件),导出一个 Client Component。要定义 Client Components,在文件顶部(在任何导入之前)添加 'use client' 指令。
    • 类似于 Pages Router,在初始页面加载时,有一个 优化步骤 将 Client Components 预渲染为静态 HTML。
  • pages/index.js 中默认导出的页面组件移动到 app/home-page.tsx
app/home-page.tsx
TypeScript
'use client'
 
// 这是一个 Client Component(与 `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 文件。这默认是一个 Server Component。

  • home-page.tsx Client Component 导入到页面中。

  • 如果你在 pages/index.js 中获取数据,使用新的 数据获取 API 直接在 Server Component 中移动数据获取逻辑。更多详情请参见 数据获取升级指南

    app/page.tsx
    TypeScript
    // 导入你的 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() {
      // 直接在 Server Component 中获取数据
      const recentPosts = await getPosts()
      // 将获取的数据转发给你的 Client Component
      return <HomePage recentPosts={recentPosts} />
    }
  • 如果你之前的页面使用了 useRouter,你需要更新到新的路由钩子。了解更多

  • 启动开发服务器并访问 http://localhost: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 来监听页面更改。有关更多详情,请参见 路由事件 部分。
  • 这些新钩子仅在 Client Components 中受支持。它们不能在 Server Components 中使用。
app/example-client-component.tsx
TypeScript
'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 路由器之间的兼容性,请参考 useRouter 钩子的 next/compat/router 导出。 这是来自 pages 目录的 useRouter 钩子,但旨在在路由器之间共享组件时使用。一旦你准备好只在 app 路由器上使用它,就更新为来自 next/navigation 的新 useRouter

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

pages 目录使用 getServerSidePropsgetStaticProps 为页面获取数据。在 app 目录中,这些以前的数据获取函数被基于 fetch()async React Server Components 构建的 更简单的 API 所替代。

app/page.tsx
TypeScript
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 Router 中,我们可以使用 Server Components 在 React 组件内部同时定位数据获取。这使我们能够向客户端发送更少的 JavaScript,同时保持从服务器渲染的 HTML。

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

app/dashboard/page.tsx
TypeScript
// `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
TypeScript
// `app` 目录
import { cookies, headers } from 'next/headers'
 
async function getData() {
  const authHeader = (await headers()).get('authorization')
 
  return '...'
}
 
export default async function Page() {
  // 你可以在 Server Components 中直接使用 `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,以在生成页面时显示 fallback 页面,设置为 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 中的动态段会按需生成。
  • false:未包含在 generateStaticParams 中的动态段将返回 404。

这替代了 pages 目录中 getStaticPathsfallback: true | false | 'blocking' 选项。dynamicParams 中不包含 fallback: 'blocking' 选项,因为随着流的出现,'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(默认值)时,当请求一个尚未生成的路由段时,它将在服务器上渲染并缓存。

增量静态再生成 (getStaticPropsrevalidate)

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 目录中被 Route Handlers 取代。

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

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

值得注意的是:如果你之前使用 API 路由从客户端调用外部 API,现在可以使用 Server Components 代替安全地获取数据。了解更多关于 数据获取

单页应用程序

如果你同时从单页应用程序 (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 Router 和 Pages Router

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

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

代码模块

Next.js 提供代码模块转换来帮助在功能被弃用时升级你的代码库。查看 代码模块 获取更多信息。