從 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 Router,其中包含新功能和慣例。新的 Router 在 app
目錄中可用,並與 pages
目錄共存。
升級到 Next.js 13 不 需要使用 App Router。您可以繼續使用 pages
以及在兩個目錄中皆可運作的新功能,例如更新的 Image 組件、Link 組件、Script 組件,以及 字體最佳化。
<Image/>
組件
Next.js 12 透過臨時匯入:next/future/image
,為 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>
,並允許您將屬性轉發到基礎標籤。
例如
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
匯入到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 預設為 伺服器組件。這與pages
目錄不同,在pages
目錄中,頁面是 客戶端組件。 - 資料fetching(資料擷取) 在
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 Component(頁面元件)移至新的 Client Component(客戶端元件)中。
- 步驟 2:將新的 Client Component(客戶端元件)匯入到
app
目錄內新的page.js
檔案中。
小知識:這是最簡單的遷移路徑,因為它與
pages
目錄的行為最為相似。
步驟 1:建立新的 Client Component(客戶端元件)
- 在
app
目錄內建立新的獨立檔案(例如app/home-page.tsx
或類似檔案),匯出 Client Component(客戶端元件)。若要定義 Client Component(客戶端元件),請在檔案頂端(任何 import 之前)新增'use client'
指令。- 與 Pages Router(頁面路由器)類似,有一個 最佳化步驟,可以在初始頁面載入時將 Client Component(客戶端元件)預先渲染為靜態 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
檔案。這預設為 Server Component(伺服器元件)。 -
將
home-page.tsx
Client Component(客戶端元件)匯入到此頁面。 -
如果您先前在
pages/index.js
中執行資料fetching(資料擷取),請使用新的 資料fetching API,將資料fetching(資料擷取)邏輯直接移至 Server Component(伺服器元件)中。請參閱 資料fetching(資料擷取)升級指南 以取得更多詳細資訊。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
,您需要更新為新的路由 hooks(鉤子)。深入瞭解。 -
啟動您的開發伺服器並訪問
https://127.0.0.1:3000
。您應該會看到現有的 index 路由,現在透過 app 目錄提供服務。
步驟 5:遷移路由 Hooks(鉤子)
已新增新的路由器以支援 app
目錄中的新行為。
在 app
中,您應該使用從 next/navigation
匯入的三個新 hooks(鉤子):useRouter()
、usePathname()
和 useSearchParams()
。
- 新的
useRouter
hook(鉤子)是從next/navigation
匯入的,並且與從next/router
匯入的pages
中的useRouter
hook(鉤子)行為不同。app
目錄中不支援從next/router
匯入的useRouter
hook(鉤子),但可以繼續在pages
目錄中使用。
- 新的
useRouter
不會傳回pathname
字串。請改用獨立的usePathname
hook(鉤子)。 - 新的
useRouter
不會傳回query
物件。搜尋參數和動態路由參數現在是分開的。請改用useSearchParams
和useParams
hooks(鉤子)。 - 您可以一起使用
useSearchParams
和usePathname
來監聽頁面變更。請參閱 Router Events(路由器事件) 章節以取得更多詳細資訊。 - 這些新的 hooks(鉤子)僅在 Client Component(客戶端元件)中受支援。它們不能在 Server Component(伺服器元件)中使用。
'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:遷移資料 Fetching(資料擷取)方法
pages
目錄使用 getServerSideProps
和 getStaticProps
來為頁面fetching(擷取)資料。在 app
目錄中,這些先前的資料fetching(資料擷取)函式已被建立在 fetch()
和 async
React Server Components(非同步 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
用於在伺服器上fetching(擷取)資料,並將 props 轉發到檔案中預設匯出的 React 元件。頁面的初始 HTML 從伺服器預先渲染,然後在瀏覽器中「hydration(注水)」頁面(使其具有互動性)。
// `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(伺服器元件),將我們的資料fetching(資料擷取)共置於 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 Object(請求物件)
在 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,並且可以在 Server Components(伺服器元件) 內部使用以檢索請求標頭。cookies
:基於 Web Cookies API,並且可以在 Server Components(伺服器元件) 內部使用以檢索 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 或直接從資料庫fetching(擷取)資料,並在建置期間將此資料向下傳遞到整個頁面。
// `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()
進行資料fetching(資料擷取)預設為 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,用於傳回路由參數,並且可以在 layouts(版面配置) 內部使用。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
with 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()
進行資料fetching(資料擷取)可以使用 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(伺服器元件) 來安全地fetching(擷取)資料。深入瞭解 資料fetching(資料擷取)。
單頁應用程式
如果您也同時從單頁應用程式 (SPA) 遷移到 Next.js,請參閱我們的 文件 以深入瞭解。
步驟 7:樣式設定
在 pages
目錄中,全域樣式表僅限於 pages/_app.js
。使用 app
目錄,此限制已解除。全域樣式可以新增至任何 layout(版面配置)、頁面或元件。
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
的自動連結預先fetching(預取)將不會跨路由器預先fetching(預取)。
相反地,您可以最佳化 App Router(應用程式路由器)和 Pages Router(頁面路由器)之間的導航,以保留預先fetching(預取)和快速頁面轉換。深入瞭解。
Codemods
Next.js 提供 Codemod 轉換,以協助您在功能被棄用時升級程式碼庫。請參閱 Codemods 以取得更多資訊。
這有幫助嗎?