跳至內容
建置您的應用程式升級應用程式路由器遷移

應用程式路由器漸進式採用指南

本指南將協助您

升級

Node.js 版本

最低 Node.js 版本現在是 v18.17。更多資訊請參閱 Node.js 文件

Next.js 版本

要更新到 Next.js 版本 13,請使用您慣用的套件管理器執行以下指令

終端機
npm install next@latest react@latest react-dom@latest

ESLint 版本
終端機
npm install -D eslint-config-next@latest

注意事項:您可能需要在 VS Code 中重新啟動 ESLint 伺服器才能使 ESLint 變更生效。開啟指令面板(Mac 上為 cmd+shift+p;Windows 上為 ctrl+shift+p),然後搜尋 ESLint: 重新啟動 ESLint 伺服器

後續步驟

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

升級新功能

Next.js 13 推出了新的 應用程式路由器 (App Router),其中包含新的功能和慣例。新的路由器可在 app 目錄中使用,並與 pages 目錄共存。

升級到 Next.js 13 並**不**需要使用新的 應用程式路由器 (App Router)。您可以繼續使用 pages 目錄,並使用兩個目錄皆適用的新功能,例如更新的 圖片元件連結元件Script 元件字體最佳化

<Image/> 元件 next-image-to-legacy-image 程式碼修改器:安全且自動地將 next/image 導入重新命名為 next/legacy/image。現有的元件將維持相同的行為。
  • next-image-experimental 程式碼修改器:危險地新增內嵌樣式並移除未使用的屬性。這會將現有元件的行為變更為符合新的預設值。要使用此程式碼修改器,您需要先執行 next-image-to-legacy-image 程式碼修改器。
  • <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 內的新 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 目錄。

    步驟二:建立根佈局

    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 providers,則需要將它們移至客戶端元件

    getLayout() 模式遷移到佈局(選用) 將屬性添加到頁面元件以實現每頁佈局。此模式可以替換為 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' // 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.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 目錄中,React 元件 next/head 用於管理 <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 '...'
    }

    查看所有中繼資料選項.

    步驟 4:遷移頁面

    • app 目錄中的頁面預設為伺服器元件。這與 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:將預設匯出的頁面元件移至新的客戶端元件。
    • 步驟 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 導入的,其行為與從 next/router 導入的 pages 中的 useRouter Hook 不同。
    • 新的 useRouter 不會返回 pathname 字串。請改用單獨的 usePathname Hook。
    • 新的 useRouter 不會返回 query 物件。搜尋參數和動態路由參數現在是分開的。請改用 useSearchParamsuseParams Hook。
    • 您可以同時使用 useSearchParamsusePathname 來監聽頁面變化。詳情請參閱路由器事件章節。
    • 這些新的 Hook 僅在客戶端元件中受支援。它們不能在伺服器元件中使用。
    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 值已被移除,因為內建的 Next.js i18n 功能在 app 目錄中不再需要。深入瞭解 i18n
    • basePath 已移除。替代方案不會成為 useRouter 的一部分。它尚未實作。
    • asPath 已移除,因為 as 的概念已從新的路由器中移除。
    • isReady 已移除,因為它不再需要。在靜態渲染期間,任何使用 useSearchParams() Hook 的元件都會略過預渲染步驟,而是在執行階段於客戶端上渲染。
    • route 已移除。usePathnameuseSelectedLayoutSegments() 提供了替代方案。

    查看 useRouter() API 參考.

    pagesapp 之間共用元件 useRouter hook。這是來自 pages 目錄的 useRouter hook,但旨在在路由器之間共享組件時使用。一旦您準備好僅在 app 路由器上使用它,請更新至 next/navigation 中新的 useRouter

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

    pages 目錄使用 getServerSidePropsgetStaticProps 來擷取頁面的資料。在 app 目錄中,這些先前的資料擷取函數已被基於 fetch()async React 伺服器組件構建的更簡單的 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 會從伺服器進行預渲染,然後在瀏覽器中「水合」(使其具有互動性)。

    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 中,我們可以使用伺服器組件在 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>
      )
    }

    存取請求物件
    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/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,並且可以在佈局 (Layouts) 內使用。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/${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,它現在更適合單獨使用。Paths 後綴被 Params 取代,這更適合具有多個動態區段的巢狀路由。


    取代 fallback
    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 以外的參數處理方式。

    這取代了 pages 目錄中 getStaticPathsfallback: true | false | 'blocking' 選項。dynamicParams 中不包含 fallback: 'blocking' 選項,因為使用串流時,'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(預設值)時,當請求尚未生成的路由區段時,它將會被伺服器端渲染並快取。

    增量靜態再生(具有 revalidategetStaticProps

    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 目錄中的 路由處理常式 取代。

    路由處理常式允許您使用 Web 請求回應 API 為給定路由建立自訂請求處理常式。

    app/api/route.ts
    export async function GET(request: Request) {}

    貼心提醒:如果您先前使用 API 路由從客戶端呼叫外部 API,現在您可以改用伺服器元件來安全地擷取資料。深入瞭解資料擷取

    步驟 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 設定樣式

    程式碼修改器

    Next.js 提供程式碼修改器轉換,以協助在功能棄用時升級您的程式碼庫。請參閱程式碼修改器以取得更多資訊。