跳到內容

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

下一步

更新後,請參閱以下章節以了解後續步驟

升級新功能

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> 元件 不再需要手動新增 <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 的行為已更新為同時支援 pagesapp,但需要進行一些變更以確保順利遷移

  • 將您先前包含在 _document.js 中的任何 beforeInteractive 腳本移至根版面配置檔案 (app/layout.tsx)。
  • 實驗性 worker 策略尚無法在 app 中運作,以這種策略標記的腳本必須移除或修改為使用不同的策略(例如 lazyOnload)。
  • onLoadonReadyonError 處理常式將無法在伺服器元件中運作,因此請務必將它們移至 客戶端元件 或完全移除。

字體最佳化

先前,Next.js 透過 內嵌字體 CSS 來協助您最佳化字體。版本 13 引入了新的 next/font 模組,讓您能夠自訂字體載入體驗,同時仍確保出色的效能和隱私。next/fontpagesapp 目錄中皆受到支援。

雖然 內嵌 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.jslayout.js
    • 使用 page.js 定義路由獨有的 UI。
    • 使用 layout.js 定義跨多個路由共用的 UI。
    • .js.jsx.tsx 檔案副檔名可用於特殊檔案。
  • 您可以將其他檔案共置在 app 目錄中,例如元件、樣式、測試等。深入了解
  • 資料抓取函式(例如 getServerSidePropsgetStaticProps)已替換為 app 內部的 新 APIgetStaticPaths 已替換為 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 內所有路由的 根版面配置

app/layout.tsx
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.tsxpages/_document.tsx 檔案。
  • .js.jsx.tsx 副檔名可用於版面配置檔案。

若要管理 <head> HTML 元素,您可以使用 內建的 SEO 支援

app/layout.tsx
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 目錄中對 巢狀版面配置 的原生支援。

請參閱遷移前和遷移後範例

遷移前

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 屬性,並遵循 遷移 Pages 的步驟app 目錄。

    app/dashboard/page.js
    export 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.js
    import 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 元素,例如 titlemeta 。在 app 目錄中,next/head 已替換為新的 內建 SEO 支援

遷移前

pages/index.tsx
import Head from 'next/head'
 
export default function Page() {
  return (
    <>
      <Head>
        <title>My page title</title>
      </Head>
    </>
  )
}

遷移後

app/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'My Page Title',
}
 
export default function Page() {
  return '...'
}

查看所有 metadata 選項.

步驟 4:遷移 Pages

  • app 目錄中的 Pages 預設為 伺服器元件。這與 pages 目錄不同,在 pages 目錄中,頁面為 客戶端元件
  • 資料抓取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:將預設匯出的 Page 元件移至新的客戶端元件。
  • 步驟 2:將新的客戶端元件匯入 app 目錄內的新 page.js 檔案。

小提示:這是最簡單的遷移路徑,因為它與 pages 目錄的行為最相似。

步驟 1:建立新的客戶端元件

  • app 目錄內建立新的獨立檔案(即 app/home-page.tsx 或類似檔案),其中匯出客戶端元件。若要定義客戶端元件,請將 'use client' 指令新增至檔案頂端(在任何匯入之前)。
    • 與 Pages Router 類似,有一個 最佳化步驟,可將客戶端元件預先渲染至初始頁面載入時的靜態 HTML。
  • 將預設匯出的頁面元件從 pages/index.js 移至 app/home-page.tsx
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 物件。搜尋參數和動態路由參數現在是分開的。請改用 useSearchParamsuseParams Hook。
  • 您可以同時使用 useSearchParamsusePathname 來監聽頁面變更。請參閱路由事件章節以了解更多詳細資訊。
  • 這些新的 Hook 僅在 Client Components 中受到支援。它們無法在 Server Components 中使用。
app/example-client-component.tsx
'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被取代
  • localelocalesdefaultLocalesdomainLocales 值已被移除,因為在 app 目錄中不再需要內建的 i18n Next.js 功能。深入了解 i18n
  • basePath 已被移除。替代方案將不會是 useRouter 的一部分。它尚未實作。
  • asPath 已被移除,因為新的路由器已移除 as 的概念。
  • isReady 已被移除,因為它不再必要。在靜態渲染期間,任何使用 useSearchParams() Hook 的元件都會略過預先渲染步驟,而改為在執行階段於用戶端渲染。
  • route 已被移除。usePathnameuseSelectedLayoutSegments() 提供了替代方案。

查看 useRouter() API 參考.

pagesapp 之間共用元件

為了保持元件在 pagesapp 路由器之間的相容性,請參考 next/compat/router 中的 useRouter Hook。這是來自 pages 目錄的 useRouter Hook,但旨在用於在路由器之間共用元件時使用。一旦您準備好僅在 app 路由器上使用它,請更新為新的 來自 next/navigationuseRouter

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

pages 目錄使用 getServerSidePropsgetStaticProps 來獲取頁面資料。在 app 目錄中,這些先前的資料獲取函式已被建立在 fetch()async React Server Components 之上的更簡單的 API 取代。

app/page.tsx
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/dashboard.js
// `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/dashboard/page.tsx
// `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/index.js
// `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/page.tsx
// `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/index.js
// `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/page.js
// `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/posts/[id].js
// `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/posts/[id]/page.js
// `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 目錄中的新模型,使用名稱 generateStaticParamsgetStaticPaths 更合適。get 字首被更具描述性的 generate 取代,現在 getStaticPropsgetServerSideProps 不再必要,因此 generate 單獨使用更合適。Paths 字尾被 Params 取代,對於具有多個動態區段的巢狀路由來說,Params 更合適。


取代 fallback

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

pages/posts/[id].js
// `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 目錄中 getStaticPathsfallback: true | false | 'blocking' 選項。fallback: 'blocking' 選項未包含在 dynamicParams 中,因為使用串流時,'blocking'true 之間的差異可以忽略不計。

app/posts/[id]/page.js
// `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(預設值)時,當請求尚未生成的路由區段時,它將會進行伺服器渲染並快取。

增量靜態再生 (getStaticPropsrevalidate)

pages 目錄中,getStaticProps 函式允許您新增 revalidate 欄位,以便在一定時間後自動重新生成頁面。

pages/index.js
// `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/page.js
// `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 RequestResponse API 為給定路由建立自訂請求處理程序。

app/api/route.ts
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 檔案中

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 檔案中匯入您的全域樣式

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

相反地,您可以最佳化 App Router 和 Pages Router 之間的導航,以保留預取和快速頁面轉換。深入了解

Codemods

Next.js 提供了 Codemod 轉換,以協助您在功能被棄用時升級程式碼庫。請參閱 Codemods 以了解更多資訊。