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,带来了新的功能和约定。新的 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:危险地添加内联样式并删除未使用的属性。这将改变现有组件的行为以匹配新的默认设置。要使用这个 codemod,你需要先运行 next-image-to-legacy-image codemod。

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

例如:

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 codemod

<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 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
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: "首页",
  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
TypeScript
import Head from "next/head";
 
export default function Page() {
  return (
    <>
      <Head>
        <title>我的页面标题</title>
      </Head>
    </>
  );
}

之后:

app/page.tsx
TypeScript
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' 指令。
    • 与 Pages Router 类似,有一个优化步骤,在初始页面加载时将客户端组件预渲染为静态 HTML。
  • pages/index.js 中默认导出的页面组件移到 app/home-page.tsx
app/home-page.tsx
TypeScript
"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
    TypeScript
    // 导入你的客户端组件
    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,你需要更新为新的路由 hook。了解更多

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

步骤 5:迁移路由 hook

为了支持 app 目录中的新行为,添加了一个新的路由器。

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

  • 新的 useRouter hook 从 next/navigation 导入,其行为与 pages 中从 next/router 导入的 useRouter hook 不同。
  • 新的 useRouter 不返回 pathname 字符串。请改用单独的 usePathname hook。
  • 新的 useRouter 不再返回 query 对象。搜索参数和动态路由参数现在是分开的。请改用 useSearchParamsuseParams hooks。
  • 你可以同时使用 useSearchParamsusePathname 来监听页面变化。有关更多详细信息,请参阅路由器事件部分。
  • 这些新 hook 仅在客户端组件中受支持。它们不能在服务器组件中使用。
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 hook 有以下变化:

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

查看 useRouter() API 参考

pagesapp 之间共享组件

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

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

pages 目录使用 getServerSidePropsgetStaticProps 来为页面获取数据。在 app 目录中,这些先前的数据获取函数被替换为基于 fetch() 和异步 React 服务器组件的更简单的 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() {
  // 你可以直接在服务器组件中使用 `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,并且可以在 layouts 中使用。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/${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 目录的新模型中,使用 generateStaticParams 这个名称比 getStaticPaths 更合适。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 中的动态段将按需生成。
  • 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 目录中,它们已被 路由处理程序 替代。

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

app/api/route.ts
export async function GET(request: Request) {}
app/api/route.js
export async function GET(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}", // <-- 添加这一行
    "./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 提供了代码转换工具,以帮助在功能被弃用时升级你的代码库。有关更多信息,请参阅 代码转换工具