如何從 Pages Router 遷移至 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 伺服器才能使變更生效。開啟命令面板(Mac 上按 cmd+shift+p;Windows 上按 ctrl+shift+p)並搜尋 ESLint: Restart ESLint Server

後續步驟

更新完成後,請參閱以下章節進行後續操作:

升級新功能

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 的預設值。

有兩個程式碼轉換工具可協助您遷移至新的 Image 元件:

  • next-image-to-legacy-image 程式碼轉換:安全且自動地將 next/image 導入重新命名為 next/legacy/image。現有元件將保持相同行為。
  • next-image-experimental 程式碼轉換:危險地添加內聯樣式並移除未使用的屬性。這將改變現有元件的行為以符合新的預設值。使用此程式碼轉換前,需先執行 next-image-to-legacy-image 程式碼轉換。

<Link> 元件 不再需要手動添加 <a> 標籤作為子元素。此行為在 版本 12.2 中作為實驗性選項添加,現在成為預設值。在 Next.js 13 中,<Link> 總是渲染 <a> 並允許您將屬性傳遞給底層標籤。

例如:

import Link from 'next/link'

// Next.js 12: `<a>` 必須嵌套,否則會被排除
<Link href="/about">
  <a>About</a>
</Link>

// Next.js 13: `<Link>` 總是渲染底層的 `<a>`
<Link href="/about">
  About
</Link>

要將您的連結升級至 Next.js 13,可以使用 new-link 程式碼轉換

<Script> 元件

next/script 的行為已更新以支援 pagesapp,但需要進行一些更改以確保順利遷移:

  • 將之前包含在 _document.js 中的任何 beforeInteractive 腳本移至根佈局檔案(app/layout.tsx)。
  • 實驗性的 worker 策略尚不適用於 app,標記為此策略的腳本需要移除或修改為使用其他策略(例如 lazyOnload)。
  • onLoadonReadyonError 處理程序在伺服器元件中不起作用,因此請確保將它們移至 客戶端元件 或完全移除。

字體優化

先前,Next.js 透過 內聯字體 CSS 協助您優化字體。版本 13 引入了新的 next/font 模組,讓您能夠自定義字體載入體驗,同時確保出色的效能和隱私。next/font 同時支援 pagesapp 目錄。

雖然 內聯 CSSpages 中仍然有效,但在 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.jslayout.js
    • 使用 page.js 定義路由特有的 UI。
    • 使用 layout.js 定義跨多個路由共享的 UI。
    • 特殊檔案可以使用 .js.jsx.tsx 副檔名。
  • 您可以在 app 目錄中並置其他檔案,例如元件、樣式、測試等。了解更多
  • 資料獲取函數如 getServerSidePropsgetStaticProps 已被 app 中的 新 API 取代。getStaticPaths 已被 generateStaticParams 取代。
  • pages/_app.jspages/_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({
  // 佈局必須接受 children 屬性。
  // 這將被嵌套佈局或頁面填充
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
  • app 目錄 必須 包含根佈局。
  • 根佈局必須定義 <html><body> 標籤,因為 Next.js 不會自動建立它們。
  • 根佈局取代了 pages/_app.tsxpages/_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 目錄中對 嵌套佈局 的原生支援取代。

查看前後範例

之前

components/DashboardLayout.js
export default function DashboardLayout({ children }) {
  return (
    <div>
      <h2>My Dashboard</h2>
      {children}
    </div>
  )
}
pages/dashboard/index.js
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.js
    export default function Page() {
      return <p>My Page</p>
    }
  • DashboardLayout 的內容移至新的 客戶端元件 以保留 pages 目錄的行為。

    app/dashboard/DashboardLayout.js
    'use client' // 此指令應位於檔案頂部,任何導入之前。
    
    // 這是客戶端元件
    export default function DashboardLayout({ children }) {
      return (
        <div>
          <h2>My Dashboard</h2>
          {children}
        </div>
      )
    }
  • DashboardLayout 導入到 app 目錄中的新 layout.js 檔案。

    app/dashboard/layout.js
    import DashboardLayout from './DashboardLayout'
    
    // 這是伺服器元件
    export default function Layout({ children }) {
      return <DashboardLayout>{children}</DashboardLayout>
    }
  • 您可以逐步將 DashboardLayout.js(客戶端元件)中的非互動部分移至 layout.js(伺服器元件),以減少傳送至客戶端的元件 JavaScript 量。

步驟 3:遷移 next/head

pages 目錄中,next/head React 元件用於管理 <head> HTML 元素,如 titlemeta。在 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 '...'
}

查看所有 metadata 選項

步驟 4:遷移頁面

  • app 目錄 中的頁面預設為 伺服器元件 (Server Components)。這與 pages 目錄不同,後者中的頁面是 客戶端元件 (Client Components)
  • 資料獲取 (Data fetching)app 目錄中有所改變。getServerSidePropsgetStaticPropsgetInitialProps 已被更簡單的 API 取代。
  • app 目錄使用巢狀資料夾來定義路由,並使用特殊的 page.js 檔案來公開存取路由片段。
  • pages 目錄app 目錄路由
    index.jspage.js/
    about.jsabout/page.js/about
    blog/[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'

// 這是一個客戶端元件(與 `pages` 目錄中的元件相同)
// 它接收資料作為 props,可以存取狀態和效果,並且在
// 初始頁面載入時在伺服器上預渲染。
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 將資料獲取邏輯直接移動到伺服器元件中。詳情請參閱 資料獲取升級指南

    // 匯入您的客戶端元件
    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() {
      // 直接在伺服器元件中獲取資料
      const recentPosts = await getPosts()
      // 將獲取的資料傳遞給您的客戶端元件
      return <HomePage recentPosts={recentPosts} />
    }
  • 如果您的舊頁面使用了 useRouter,您需要更新為新的路由鉤子。了解更多

  • 啟動您的開發伺服器並訪問 http://localhost:3000。您應該會看到現有的索引路由,現在是通過 app 目錄提供的。

步驟 5:遷移路由鉤子

新增了一個新的路由器來支援 app 目錄中的新行為。

app 目錄中,您應該使用從 next/navigation 匯入的三個新鉤子:useRouter()usePathname()useSearchParams()

  • 新的 useRouter 鉤子是從 next/navigation 匯入的,其行為與從 next/router 匯入的 pages 中的 useRouter 鉤子不同。
  • 新的 useRouter 不返回 pathname 字串。請改用獨立的 usePathname 鉤子。
  • 新的 useRouter 不返回 query 物件。搜尋參數和動態路由參數現在是分開的。請改用 useSearchParamsuseParams 鉤子。
  • 您可以一起使用 useSearchParamsusePathname 來監聽頁面變化。詳情請參閱 路由器事件 (Router Events) 部分。
  • 這些新鉤子僅在客戶端元件中受支援。它們不能在伺服器元件中使用。
'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 已被 取代
  • localelocalesdefaultLocalesdomainLocales 值已被移除,因為內建的 i18n Next.js 功能在 app 目錄中不再需要。了解更多關於 i18n
  • basePath 已被移除。替代方案不會是 useRouter 的一部分。它尚未實現。
  • asPath 已被移除,因為 as 的概念已從新路由器中移除。
  • isReady 已被移除,因為它不再必要。在 靜態渲染 (static rendering) 期間,任何使用 useSearchParams() 鉤子的元件將跳過預渲染步驟,而是在運行時在客戶端渲染。
  • route 已被移除。usePathnameuseSelectedLayoutSegments() 提供了替代方案。

查看 useRouter() API 參考

pagesapp 之間共享元件

要保持元件在 pagesapp 路由器之間相容,請參考 next/compat/router 匯入的 useRouter 鉤子。 這是 pages 目錄中的 useRouter 鉤子,但旨在用於在路由器之間共享元件。當您準備好僅在 app 路由器中使用它時,請更新為新的 next/navigation 匯入的 useRouter

步驟 6:遷移資料獲取方法

pages 目錄使用 getServerSidePropsgetStaticProps 來獲取頁面的資料。在 app 目錄中,這些先前的資料獲取函數被替換為基於 fetch()async React 伺服器元件的 更簡單的 API

export default async function Page() {
  // 此請求應被快取直到手動失效。
  // 類似於 `getStaticProps`。
  // `force-cache` 是預設值,可以省略。
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })

  // 此請求應在每次請求時重新獲取。
  // 類似於 `getServerSideProps`。
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })

  // 此請求應被快取,有效期為 10 秒。
  // 類似於 `getStaticProps` 的 `revalidate` 選項。
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })

  return <div>...</div>
}

伺服器端渲染 (getServerSideProps)

pages 目錄中,getServerSideProps 用於在伺服器上獲取資料並將 props 傳遞給檔案中的預設匯出 React 元件。頁面的初始 HTML 從伺服器預渲染,然後在瀏覽器中「水合」(hydrating) 頁面(使其具有互動性)。

pages/dashboard.js
// `pages` 目錄

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` 目錄

// 此函數可以命名為任何名稱
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/index.js
// `pages` 目錄

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` 目錄
import { cookies, headers } from 'next/headers'

async function getData() {
  const authHeader = (await headers()).get('authorization')

  return '...'
}

export default async function Page() {
  // 您可以直接在伺服器元件中使用 `cookies` 或 `headers`,
  // 或在您的資料獲取函數中使用
  const theme = (await cookies()).get('theme')
  const data = await getData()
  return '...'
}

靜態網站生成 (getStaticProps)

pages 目錄中,getStaticProps 函數用於在構建時預渲染頁面。此函數可用於從外部 API 或直接從資料庫獲取資料,並在構建期間將此資料傳遞給整個頁面。

pages/index.js
// `pages` 目錄

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/page.js
// `app` 目錄

// 此函數可以命名為任何名稱
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/posts/[id].js
// `pages` 目錄
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/posts/[id]/page.js
// `app` 目錄
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 取代,這在 getStaticPropsgetServerSideProps 不再必要的情況下更為貼切。Paths 後綴被 Params 取代,這對於具有多個動態片段的路由更為合適。


取代 fallback

pages 目錄中,getStaticPaths 返回的 fallback 屬性用於定義未在建置時預渲染的頁面行為。此屬性可以設置為 true 以在頁面生成時顯示回退頁面,false 以顯示 404 頁面,或 blocking 以在請求時生成頁面。

pages/posts/[id].js
// `pages` 目錄

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 目錄中 getStaticPathsfallback: true | false | 'blocking' 選項。fallback: 'blocking' 選項未包含在 dynamicParams 中,因為在串流的情況下,'blocking'true 之間的差異可以忽略不計。

app/posts/[id]/page.js
// `app` 目錄

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(預設值)時,當請求尚未生成的路由片段時,它將被伺服器渲染並緩存。

增量靜態再生 (getStaticPropsrevalidate)

pages 目錄中,getStaticProps 函數允許你添加 revalidate 字段以在特定時間後自動重新生成頁面。

pages/index.js
// `pages` 目錄

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/page.js
// `app` 目錄

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) 取代。

路由處理器允許你使用 Web RequestResponse API 為給定路由創建自定義請求處理器。

export async function GET(request: Request) {}

須知:如果你之前使用 API 路由從客戶端調用外部 API,現在可以使用 伺服器組件 (Server Components) 來安全地獲取數據。了解更多關於 數據獲取 (data fetching) 的資訊。

單頁應用程式 (SPA)

如果你同時從單頁應用程式 (SPA) 遷移到 Next.js,請參閱我們的 文檔 以了解更多。

步驟 7:樣式設定

pages 目錄中,全局樣式表僅限於 pages/_app.js。在 app 目錄中,此限制已被取消。全局樣式可以添加到任何布局、頁面或組件中。

Tailwind CSS

如果你使用 Tailwind CSS,你需要在 tailwind.config.js 文件中添加 app 目錄:

tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}', // <-- 添加此行
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
  ],
}

你還需要在 app/layout.js 文件中導入全局樣式:

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 的自動鏈接預取不會跨路由器預取。

相反,你可以 優化導航 以保留預取和快速的頁面轉換。 了解更多

Codemods

Next.js 提供了 Codemod 轉換來幫助你在功能被棄用時升級代碼庫。詳見 Codemods 以獲取更多資訊。