从 Pages 到 App
本指南将帮助您
升级
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
。
后续步骤
更新后,请参阅以下部分以获取后续步骤
- 升级新功能:帮助您升级到新功能(例如改进的 Image 和 Link 组件)的指南。
- 从
pages
迁移到app
目录:帮助您逐步从pages
迁移到app
目录的分步指南。
升级新功能
Next.js 13 引入了新的 App 路由器,并附带了新功能和约定。新的路由器可在 app
目录中使用,并与 pages
目录共存。
升级到 Next.js 13 **不需要** 使用新的 App 路由器。您可以继续使用 pages
,并使用适用于这两个目录的新功能,例如更新的 图片组件、链接组件、脚本组件 和 字体优化。
<Image/>
组件
Next.js 12 引入了图片组件的新改进,并使用临时导入:next/future/image
。这些改进包括减少客户端 JavaScript、更轻松地扩展和设置图片样式、更好的可访问性和原生浏览器延迟加载。
在 13 版本中,此新行为现在是 next/image
的默认行为。
有两个代码修改工具可以帮助您迁移到新的图片组件
next-image-to-legacy-image
代码修改工具:安全且自动地将next/image
导入重命名为next/legacy/image
。现有组件将保持相同行为。next-image-experimental
代码修改工具:危险地添加内联样式并删除未使用的属性。这会将现有组件的行为更改为匹配新的默认值。要使用此代码修改工具,您需要先运行next-image-to-legacy-image
代码修改工具。
<Link>
组件
<Link>
组件 不再需要手动添加 <a>
标签作为子元素。此行为在 12.2 版本 中添加为实验选项,现在已成为默认选项。在 Next.js 13 中,<Link>
始终呈现 <a>
并允许您将属性转发到底层标签。
例如
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
代码修改工具。
<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 路由器 → YouTube(16 分钟)。
迁移到 App 路由器可能是第一次使用 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
属性,并按照 迁移页面的步骤 迁移到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:迁移页面
app
目录 中的页面默认情况下是 服务器组件。这与pages
目录不同,在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:将默认导出的页面组件移动到一个新的客户端组件中。
- 步骤 2:将新的客户端组件导入到
app
目录中一个新的page.js
文件中。
注意:这是最简单的迁移路径,因为它与
pages
目录的行为最相似。
步骤 1:创建一个新的客户端组件
- 在
app
目录中创建一个新的单独文件(即app/home-page.tsx
或类似文件),该文件导出一个客户端组件。要定义客户端组件,请在文件顶部(在任何导入之前)添加'use client'
指令。- 与页面路由器类似,有一个 优化步骤 可以将客户端组件预渲染到初始页面加载时的静态 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://127.0.0.1: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()
和异步 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
对象并使用它来检索请求的cookie和标头。
// `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/${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
比getStaticPaths
更适合app
目录中的新模型。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'
选项。dynamicParams
中不包含fallback: 'blocking'
选项,因为使用流式传输时,'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和ResponseAPI为给定路由创建自定义请求处理程序。
export async function GET(request: Request) {}
需要了解:如果您之前使用API路由从客户端调用外部API,现在可以使用服务器组件来安全地获取数据。了解有关数据获取的更多信息。
步骤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进行样式设置的更多信息
Codemods
Next.js 提供 Codemod 变换来帮助您在功能弃用时升级代码库。有关更多信息,请参阅Codemods。
这有帮助吗?