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
。
下一步
更新後,請參閱以下章節以了解後續步驟
- 升級新功能:本指南協助您升級至新功能,例如改良的 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-image
codemod:安全且自動地將next/image
匯入重新命名為next/legacy/image
。現有的元件將維持相同的行為。next-image-experimental
codemod:危險地新增內嵌樣式並移除未使用的屬性。這會變更現有元件的行為,以符合新的預設值。若要使用此 codemod,您需要先執行next-image-to-legacy-image
codemod。
<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 建議在 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
屬性,並遵循 遷移 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
匯入新的layout.js
檔案中,該檔案位於app
目錄內。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 預設為 伺服器元件。這與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:將預設匯出的 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
,則需要更新至新的路由 Hook。深入了解。 -
啟動您的開發伺服器並造訪
https://127.0.0.1:3000
。您應該會看到您現有的索引路由,現在透過 app 目錄提供。
步驟 5:遷移路由 Hook
已新增新的路由器,以支援 app
目錄中的新行為。
在 app
中,您應該使用從 next/navigation
匯入的三個新 Hook:useRouter()
、usePathname()
和 useSearchParams()
。
- 新的
useRouter
Hook 是從next/navigation
匯入,且行為與pages
中的useRouter
Hook 不同,後者是從next/router
匯入。useRouter
Hook 從next/router
匯入 在app
目錄中不受支援,但可以繼續在pages
目錄中使用。
- 新的
useRouter
不會傳回pathname
字串。請改用個別的usePathname
Hook。 - 新的
useRouter
不會傳回query
物件。搜尋參數和動態路由參數現在是分開的。請改用useSearchParams
和useParams
Hook。 - 您可以同時使用
useSearchParams
和usePathname
來監聽頁面變更。請參閱路由事件章節以了解更多詳細資訊。 - 這些新的 Hook 僅在 Client Components 中受到支援。它們無法在 Server Components 中使用。
'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
值已被移除,因為在app
目錄中不再需要內建的 i18n Next.js 功能。深入了解 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 Server Components 之上的更簡單的 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 從伺服器預先渲染,然後在瀏覽器中「水合 (hydrating)」頁面(使其具有互動性)。
// `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 中,我們可以使用 Server Components 將資料獲取共置於我們的 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>
)
}
存取 Request 物件
在 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
目錄公開了新的唯讀函式來檢索請求資料
headers
:基於 Web Headers API,可用於 Server Components 內部以檢索請求標頭。cookies
:基於 Web Cookies API,可用於 Server Components 內部以檢索 Cookie。
// `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
取代,對於具有多個動態區段的巢狀路由來說,Params
更合適。
取代 fallback
在 pages
目錄中,從 getStaticPaths
返回的 fallback
屬性用於定義在建置時未預先渲染的頁面的行為。此屬性可以設定為 true
以在生成頁面時顯示 fallback 頁面,設定為 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
目錄中已被 Route Handlers 取代。
Route Handlers 允許您使用 Web Request 和 Response API 為給定路由建立自訂請求處理程序。
export async function GET(request: Request) {}
小提示:如果您先前使用 API 路由從用戶端呼叫外部 API,您現在可以使用 Server Components 來安全地獲取資料。深入了解資料獲取。
單頁應用程式
如果您也同時從單頁應用程式 (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>
)
}
將 App Router 與 Pages Router 一起使用
在由不同 Next.js 路由器提供的路由之間導航時,將會發生硬導航。使用 next/link
的自動連結預取將不會跨路由器預取。
相反地,您可以最佳化 App Router 和 Pages Router 之間的導航,以保留預取和快速頁面轉換。深入了解。
Codemods
Next.js 提供了 Codemod 轉換,以協助您在功能被棄用時升級程式碼庫。請參閱 Codemods 以了解更多資訊。
這有幫助嗎?