Skip to main content

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
note

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

下一步操作

升级后,请查看以下部分以获取下一步操作:

  • 升级新功能:帮助您升级到新功能,如改进的 Image 和 Link 组件等。
  • pages迁移到 app 目录:逐步迁移从pages到 app 目录。

升级新功能

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 的默认行为。

有两个 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 的行为已更新,以同时支持页面和 app,但需要进行一些更改以确保平滑迁移:

  • 将先于交互之前的脚本移动到先前包含在 _document.js 中的根布局文件(app/layout.tsx)。
  • 实验性的 worker 策略在 app 中尚不起作用, 使用此策略标记的脚本将需要被删除或修改为使用其他策略(例如 lazyOnload)。
  • onLoadonReadyonError 处理程序在 Server Components 中不起作用, 因此请确保将它们移到 Client Component 或将它们完全删除。

字体优化

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

虽然在页面中内联 CSS 仍然有效,但在 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.js 和 layout.js。
    • 使用 page.js 定义特定于路由的 UI。
    • 使用 layout.js 定义跨多个路由共享的 UI。
  • 可以在 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/* 目前仍然位于 pages 目录中。

步骤 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 { 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 上下文提供程序,则需要将它们移至 Client Component。

步骤 3:迁移 next/head

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

Before:

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

export default function Page() {
return (
<>
<Head>
<title>My page title</title>
</Head>
</>
)
}

After:

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

export const metadata: Metadata = {
title: 'My Page Title',
}

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

查看所有元数据选项。

步骤 4:迁移页面

app 目录中,默认情况下,页面是 Server Components。 这与 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:创建新的 Client Component

在 app 目录内创建一个新的文件(例如 app/home-page.tsx 或类似的), 导出一个 Client Component。为了定义 Client Components, 将 'use client' 指令添加到文件的顶部(在任何导入之前)。 将默认导出的页面组件从 pages/index.js 移动到 app/home-page.tsx

app/home-page.tsx
'use client';

// This is a Client Component. It receives data as props and
// has access to state and effects just like Page components
// in the `pages` directory.
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 中进行了数据获取,将数据获取逻辑直接移动到 Server Component 中, 使用新的数据获取 API。有关更多详细信息,请参阅数据获取升级指南。

app/page.tsx

// 导入您的 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。您应该看到您现有的 index 路由,现在通过 app 目录提供服务。

步骤 5:迁移路由钩子

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

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

  • 新的 useRouter 钩子是从 next/navigation 导入的, 与从 next/router 导入的 pages 中的 useRouter 钩子有不同的行为。
  • next/router 导入的 useRouter 钩子在 app 目录中不受支持,但可以继续在 pages 目录中使用。
  • 新的 useRouter 不返回 pathname 字符串。使用单独的 usePathname 钩子代替。
  • 新的 useRouter 不返回查询对象。使用单独的 useSearchParams 钩子代替。
  • 您可以使用 useSearchParamsusePathname 一起监听页面更改。有关更多详细信息,请参阅 Router 事件部分。
  • 这些新的钩子仅在 Client Components 中受支持。它们不能在 Server Components 中使用。
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() 钩子的任何组件都将跳过预渲染步骤,并在运行时在客户端上呈现。 查看 useRouter() API 参考。

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

pages 目录使用 getServerSidePropsgetStaticProps 来为页面获取数据。 在 app 目录中,这些以前的数据获取函数已被构建在 fetch()async React Server Components 之上的一个更简单的 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
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 组件中使用 Server Components 放置我们的数据获取。 这允许我们向客户端发送更少的 JavaScript,同时保持来自服务器的渲染 HTML。

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

app/dashboard/page.tsx
// 这个函数可以取任何名字
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
export async function getServerSideProps({ req, query }) {
const authHeader = req.headers['authorization'];
const theme = req.cookies['theme'];

return { props: { ... } };
}

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

app 目录中,可以使用新的只读函数来检索请求数据:

  • headers():基于 Web Headers API,可以在 Server Components 中用于检索请求标头。
  • cookies():基于 Web Cookies API,可以在 Server Components 中用于检索 cookie。
app/page.tsx
import { cookies, headers } from 'next/headers';

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

return '...';
}

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

静态站点生成 (getStaticProps)

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

pages/index.js
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 的返回形状是一系列段,而不是嵌套的参数对象数组或已解析路径的字符串。

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} />;
}

使用 generateStaticParams 这个名字更适合在 app 目录中的新模型中,get 前缀被更具描述性的 generate 替换,现在独立存在更好,因为 getStaticProps 和 getServerSideProps 不再需要。Paths 后缀被 Params 替换,对于具有多个动态段的嵌套路由,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 }) {
// ...
}

app目录中,config.dynamicParams属性控制在generateStaticParams之外的params的处理方式:

  • 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);

// ...
}

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

增量静态再生成(getStaticProps with revalidate

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目录中被Route Handlers替换。

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

app/api/route.ts

export async function GET(request: Request) {
// ...
}
note

如果以前使用API路由从客户端调用外部API, 现在可以使用Server Components代替以安全地获取数据。了解更多关于数据获取的信息。

步骤 7:样式

pages目录中,全局样式表仅限于pages/_app.js。 而在app目录中,解除了这一限制。全局样式可以添加到任何布局、页面或组件中。

  • CSS Modules
  • Tailwind CSS
  • 全局样式
  • CSS-in-JS
  • 外部样式表
  • Sass

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的更多样式信息。