如何从 Pages 迁移到 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: Restart ESLint Server`。
后续步骤
更新后,请参阅以下部分了解后续步骤
- 升级新功能:帮助您升级到新功能(例如改进的图像和链接组件)的指南。
- 从 `pages` 目录迁移到 `app` 目录:逐步指南,帮助您从 `pages` 目录逐步迁移到 `app` 目录。
升级新功能
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` 的默认行为。
有两个 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>
组件
`` 组件不再需要手动添加 `` 标签作为子级。此行为在 版本 12.2 中作为实验性选项添加,现在是默认行为。在 Next.js 13 中,`` 始终渲染 `` 并允许您将属性转发到底层标签。
例如
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` 的行为已更新以支持 `pages` 和 `app`,但需要进行一些更改以确保平稳迁移
- 将您之前包含在 `_document.js` 中的任何 `beforeInteractive` 脚本移动到根布局文件 (`app/layout.tsx`)。
- 实验性的 `worker` 策略在 `app` 中尚不起作用,使用此策略表示的脚本将不得不被移除或修改以使用不同的策略(例如 `lazyOnload`)。
- `onLoad`、`onReady` 和 `onError` 处理程序在服务器组件中不起作用,因此请确保将它们移动到 客户端组件 或完全移除它们。
字体优化
以前,Next.js 通过 内联字体 CSS 来帮助您优化字体。版本 13 引入了新的 `next/font` 模块,使您能够自定义字体加载体验,同时仍确保出色的性能和隐私。`next/font` 在 `pages` 和 `app` 目录中都受支持。
虽然 内联 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.js` 和 `layout.js`。
- 使用 `page.js` 定义特定于路由的 UI。
- 使用 `layout.js` 定义跨多个路由共享的 UI。
- `js`、`jsx` 或 `tsx` 文件扩展名可用于特殊文件。
- 您可以在 `app` 目录中并置其他文件,例如组件、样式、测试等。了解更多。
- `getServerSideProps` 和 `getStaticProps` 等数据获取函数已被 `app` 中 新的 API 替换。`getStaticPaths` 已被 `generateStaticParams` 替换。
- `pages/_app.js` 和 `pages/_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` 中所有路由的 根布局。
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` 目录必须包含一个根布局。
- 根布局必须定义 `` 和 `` 标签,因为 Next.js 不会自动创建它们。
- 根布局替换了 `pages/_app.tsx` 和 `pages/_document.tsx` 文件。
- 布局文件可以使用 `.js`、`.jsx` 或 `.tsx` 扩展名。
要管理 `
` HTML 元素,您可以使用 内置的 SEO 支持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 建议向 Page 组件添加一个属性,以在 `pages` 目录中实现每页布局。此模式可以被 `app` 目录中对 嵌套布局 的原生支持所取代。
查看前后示例
之前
export default function DashboardLayout({ children }) {
return (
<div>
<h2>My Dashboard</h2>
{children}
</div>
)
}
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.jsexport 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.jsimport 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 组件用于管理 `
` HTML 元素,例如 `title` 和 `meta`。在 `app` 目录中,`next/head` 已被新的 内置 SEO 支持 替换。之前
import Head from 'next/head'
export default function Page() {
return (
<>
<Head>
<title>My page title</title>
</Head>
</>
)
}
之后
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Page Title',
}
export default function Page() {
return '...'
}
步骤 4:迁移页面
- `app` 目录中的页面默认是 服务器组件。这与 `pages` 目录不同,其中页面是 客户端组件。
- `app` 中的 数据获取 已更改。`getServerSideProps`、`getStaticProps` 和 `getInitialProps` 已被更简单的 API 替换。
- `app` 目录使用嵌套文件夹定义路由,并使用特殊的 `page.js` 文件使路由段可公开访问。
-
`pages` 目录 `app` 目录 路由 index.js
page.js
/
about.js
about/page.js
/about
blog/[slug].js
blog/[slug]/page.js
/blog/post-1
我们建议将页面的迁移分解为两个主要步骤
- 步骤 1:将默认导出的 Page 组件移动到一个新的客户端组件中。
- 步骤 2:将新的客户端组件导入到 `app` 目录中的新 `page.js` 文件中。
须知:这是最简单的迁移路径,因为它与 `pages` 目录的行为最相似。
步骤 1:创建一个新的客户端组件
- 在 `app` 目录中创建一个新的单独文件(即 `app/home-page.tsx` 或类似文件),该文件导出一个客户端组件。要定义客户端组件,请在文件顶部(在任何导入之前)添加 `'use client'` 指令。
- 与 Pages Router 类似,有一个 优化步骤,可在初始页面加载时将客户端组件预渲染为静态 HTML。
- 将从 `pages/index.js` 默认导出的页面组件移动到 `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://: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` 对象。搜索参数和动态路由参数现在是分开的。请改用 `useSearchParams` 和 `useParams` 钩子。
- 您可以结合使用 `useSearchParams` 和 `usePathname` 来监听页面更改。有关更多详细信息,请参阅 路由器事件 部分。
- 这些新的钩子仅在客户端组件中受支持。它们不能在服务器组件中使用。
'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` 已 被替换。
- `locale`、`locales`、`defaultLocales`、`domainLocales` 值已移除,因为 `app` 目录中不再需要内置的 i18n Next.js 功能。了解有关 i18n 的更多信息。
- `basePath` 已被移除。替代方案将不属于 `useRouter`。它尚未实现。
- `asPath` 已被移除,因为 `as` 的概念已从新路由器中移除。
- `isReady` 已被移除,因为它不再必要。在 静态渲染 期间,任何使用 `useSearchParams()` 钩子的组件都将跳过预渲染步骤,而是在运行时在客户端上渲染。
- `route` 已被移除。`usePathname` 或 `useSelectedLayoutSegments()` 提供了一个替代方案。
在 `pages` 和 `app` 之间共享组件
为了使组件在 `pages` 和 `app` 路由器之间兼容,请参考 `next/compat/router` 中的 `useRouter` 钩子。这是 `pages` 目录中的 `useRouter` 钩子,但旨在用于在路由器之间共享组件。一旦您准备好只在 `app` 路由器上使用它,请更新到 `next/navigation` 中的新 `useRouter`。
步骤 6:迁移数据获取方法
`pages` 目录使用 `getServerSideProps` 和 `getStaticProps` 为页面获取数据。在 `app` 目录中,这些以前的数据获取函数被基于 `fetch()` 和 `async` React 服务器组件构建的 更简单的 API 替换。
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` 用于在服务器上获取数据并将属性转发到文件中默认导出的 React 组件。页面的初始 HTML 从服务器预渲染,然后在浏览器中“水合”(使其可交互)。
// `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` 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` 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` 目录公开了新的只读函数来检索请求数据
- `headers`:基于 Web Headers API,可在 服务器组件 中使用以检索请求头。
- `cookies`:基于 Web Cookies API,可在 服务器组件 中使用以检索 cookies。
// `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` 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` 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` 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` 目录中,`getStaticPaths` 被 `generateStaticParams` 替换。
`generateStaticParams` 的行为类似于 `getStaticPaths`,但它具有简化的 API,用于返回路由参数,并且可以在 布局 中使用。`generateStaticParams` 的返回形式是段数组,而不是嵌套 `param` 对象的数组或已解析路径的字符串。
// `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` 目录中的新模型,使用 `generateStaticParams` 这个名称比 `getStaticPaths` 更合适。`get` 前缀被更具描述性的 `generate` 替换,现在 `getStaticProps` 和 `getServerSideProps` 不再必要,它单独使用更好。`Paths` 后缀被 `Params` 替换,这对于具有多个动态段的嵌套路由更合适。
替换 `fallback`
在 `pages` 目录中,从 `getStaticPaths` 返回的 `fallback` 属性用于定义未在构建时预渲染的页面的行为。此属性可以设置为 `true` 以在页面生成时显示回退页面,`false` 以显示 404 页面,或 `blocking` 以在请求时生成页面。
// `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` 目录中 `getStaticPaths` 的 `fallback: true | false | 'blocking'` 选项。`fallback: 'blocking'` 选项未包含在 `dynamicParams` 中,因为在流式传输时,`'blocking'` 和 `true` 之间的差异可以忽略不计。
// `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`(默认值)时,当请求尚未生成的路由段时,它将被服务器渲染并缓存。
增量静态再生成 (`getStaticProps` 与 `revalidate`)
在 `pages` 目录中,`getStaticProps` 函数允许您添加 `revalidate` 字段,以在一定时间后自动重新生成页面。
// `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` 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 请求 和 响应 API 为给定路由创建自定义请求处理程序。
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` 文件中
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` 文件中导入您的全局样式
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 提供 Codemod 转换来帮助您升级代码库。有关更多信息,请参阅 Codemods。
这有帮助吗?