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: 重新启动 ESLint 服务器
。
后续步骤
更新后,请参阅以下部分了解后续步骤
- 升级新功能:帮助你升级到新功能(例如改进的 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 为 Image 组件引入了新的改进,并使用临时导入:next/future/image
。这些改进包括减少客户端 JavaScript、更轻松地扩展和设置图像样式、更好的可访问性和原生浏览器延迟加载。
在 13 版本中,此新行为现在是 next/image
的默认行为。
有两个代码修改可以帮助您迁移到新的 Image 组件
next-image-to-legacy-image
代码修改:安全且自动地将next/image
导入重命名为next/legacy/image
。现有组件将保持相同的行为。next-image-experimental
代码修改:危险地添加内联样式并删除未使用的 props。这会将现有组件的行为更改为与新的默认值匹配。要使用此代码修改,您需要首先运行next-image-to-legacy-image
代码修改。
<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
代码修改。
<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
属性,并按照迁移页面到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
比在app
目录中的新模型中使用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 Request 和 Response API 为给定路由创建自定义请求处理程序。
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 进行样式设置的更多信息
代码修改
Next.js 提供代码修改转换,以帮助在功能弃用时升级您的代码库。有关更多信息,请参阅代码修改。
这有帮助吗?