从 Pages 迁移到 App
本指南将帮助你
升级
Node.js 版本
最低 Node.js 版本现在是 v18.17。 请参阅 Node.js 文档 以获取更多信息。
Next.js 版本
要更新到 Next.js 版本 13,请使用你首选的包管理器运行以下命令
npm install next@latest react@latest react-dom@latestESLint 版本
如果你正在使用 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。
下一步
更新后,请参阅以下部分了解后续步骤
- 升级新功能:一份指南,帮助你升级到新功能,例如改进的 Image 和 Link 组件。
- 从
pages目录迁移到app目录:一份逐步指南,帮助你从pages逐步迁移到app目录。
升级新功能
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-imagecodemod:安全且自动地将next/image导入重命名为next/legacy/image。现有组件将保持相同的行为。next-image-experimentalcodemod:危险地添加内联样式并删除未使用的 props。这将更改现有组件的行为以匹配新的默认值。要使用此 codemod,你需要首先运行next-image-to-legacy-imagecodemod。
<Link> 组件
<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 的行为以同时支持 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目录必须包含根布局。- 根布局必须定义
<html>和<body>标签,因为 Next.js 不会自动创建它们 - 根布局替换了
pages/_app.tsx和pages/_document.tsx文件。 .js、.jsx或.tsx扩展名可用于布局文件。
要管理 <head> 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 建议向页面组件添加 属性,以在 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属性,并按照 迁移 Pages 的步骤 迁移到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 组件用于管理 <head> 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:迁移 Pages
app目录 中的页面默认是 服务器组件。这与pages目录不同,后者中的页面是 客户端组件。- 数据获取 在
app中已更改。getServerSideProps、getStaticProps和getInitialProps已被更简单的 API 替换。 app目录使用嵌套文件夹定义路由,并使用特殊的page.js文件使路由段公开可访问。-
pages目录app目录路由 index.jspage.js/about.jsabout/page.js/aboutblog/[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。
'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。你应该能看到你现有的 index 路由,现在通过 app 目录提供。
步骤 5:迁移路由 Hooks
一个新的路由器已被添加到 app 目录中,以支持新的行为。
在 app 中,你应该使用从 next/navigation 导入的三个新 hook:useRouter()、usePathname() 和 useSearchParams()。
- 新的
useRouterhook 从next/navigation导入,并且与从next/router导入的pages中的useRouterhook 具有不同的行为。- 从
next/router导入的useRouterhook 在app目录中不受支持,但可以继续在pages目录中使用。
- 从
- 新的
useRouter不返回pathname字符串。请改用单独的usePathnamehook。 - 新的
useRouter不返回query对象。搜索参数和动态路由参数现在是分开的。请改用useSearchParams和useParamshook。 - 你可以一起使用
useSearchParams和usePathname来监听页面更改。有关更多详细信息,请参阅 路由器事件 部分。 - 这些新的 hook 仅在客户端组件中受支持。它们不能在服务器组件中使用。
'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已被替换。locale、locales、defaultLocales、domainLocales值已被移除,因为内置的 i18n Next.js 功能在app目录中不再必要。了解更多关于 i18n 的信息。basePath已被移除。替代方案将不包含在useRouter中。它尚未实现。asPath已被移除,因为as的概念已从新的路由器中移除。isReady已被移除,因为它不再必要。在静态渲染期间,任何使用useSearchParams()hook 的组件都将跳过预渲染步骤,而是在运行时在客户端渲染。route已被移除。usePathname或useSelectedLayoutSegments()提供了替代方案。
在 pages 和 app 之间共享组件
为了保持组件在 pages 和 app 路由器之间的兼容性,请参考来自 next/compat/router 的 useRouter hook。这是来自 pages 目录的 useRouter hook,但旨在用于在路由器之间共享组件时使用。一旦你准备好仅在 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 用于在服务器上获取数据,并将 props 转发到文件中默认导出的 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 目录公开了新的只读函数来检索请求数据
// `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 不再必要,因此单独使用 generate 更好。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 Request 和 Response 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 之间,以保留预取和快速页面转换。了解更多。
Codemods
Next.js 提供了 Codemod 转换来帮助你在功能被弃用时升级你的代码库。请参阅 Codemods 以获取更多信息。
这有帮助吗?