連結與導航

在 Next.js 中有四種方式可以在路由之間進行導航:

本頁將介紹如何使用這些選項,並深入探討導航的運作原理。

<Link> 是一個內建元件,它擴展了 HTML 的 <a> 標籤,提供 預取 (prefetching) 和客戶端路由導航功能。這是 Next.js 中推薦的主要路由導航方式。

您可以從 next/link 導入它,並向元件傳遞 href 屬性:

import Link from 'next/link'

export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

您還可以傳遞其他可選屬性給 <Link>。詳見 API 參考文件

useRouter() 鉤子

useRouter 鉤子允許您從 客戶端元件 中以程式方式變更路由。

'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

完整的 useRouter 方法列表,請參閱 API 參考文件

建議: 除非有特殊需求需要使用 useRouter,否則建議使用 <Link> 元件進行路由導航。

redirect 函式

對於 伺服器元件,請改用 redirect 函式。

import { redirect } from 'next/navigation'

async function fetchTeam(id: string) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}

export default async function Profile({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  if (!id) {
    redirect('/login')
  }

  const team = await fetchTeam(id)
  if (!team) {
    redirect('/join')
  }

  // ...
}

重要須知:

  • redirect 預設返回 307 (臨時重定向) 狀態碼。在伺服器動作 (Server Action) 中使用時,它會返回 303 (查看其他),這通常用於在 POST 請求後重定向到成功頁面。
  • redirect 內部會拋出錯誤,因此應在 try/catch 區塊外呼叫。
  • redirect 可以在客戶端元件的渲染過程中被呼叫,但不能在事件處理程序中呼叫。這種情況下應改用 useRouter 鉤子
  • redirect 也接受絕對 URL,可用於重定向到外部連結。
  • 如果您希望在渲染過程之前進行重定向,請使用 next.config.js中介軟體 (Middleware)

更多資訊請參閱 redirect API 參考文件

使用原生 History API

Next.js 允許您使用原生的 window.history.pushStatewindow.history.replaceState 方法來更新瀏覽器的歷史記錄堆疊,而無需重新載入頁面。

pushStatereplaceState 的呼叫會與 Next.js 路由器整合,使您可以與 usePathnameuseSearchParams 同步。

window.history.pushState

用於向瀏覽器的歷史記錄堆疊添加新條目。使用者可以導航回先前的狀態。例如,用於對產品列表進行排序:

'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>升冪排序</button>
      <button onClick={() => updateSorting('desc')}>降冪排序</button>
    </>
  )
}
'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>升冪排序</button>
      <button onClick={() => updateSorting('desc')}>降冪排序</button>
    </>
  )
}

window.history.replaceState

用於替換瀏覽器歷史記錄堆疊中的當前條目。使用者無法導航回先前的狀態。例如,用於切換應用程式的語言環境:

'use client'

import { usePathname } from 'next/navigation'

export function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale: string) {
    // 例如 '/en/about' 或 '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>Français</button>
    </>
  )
}
'use client'

import { usePathname } from 'next/navigation'

export function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale) {
    // 例如 '/en/about' 或 '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>Français</button>
    </>
  )
}

路由與導航的運作原理

應用程式路由器 (App Router) 使用混合方法來處理路由和導航。在伺服器端,您的應用程式代碼會按路由區段自動進行 代碼分割 (code-splitting)。在客戶端,Next.js 會 預取 (prefetch)快取 (cache) 路由區段。這意味著當使用者導航到新路由時,瀏覽器不會重新載入頁面,只有變更的路由區段會重新渲染,從而提升導航體驗和效能。

1. 代碼分割 (Code Splitting)

代碼分割允許您將應用程式代碼拆分為較小的套件,供瀏覽器下載和執行。這減少了每次請求傳輸的數據量和執行時間,從而提高效能。

伺服器元件 允許您的應用程式代碼按路由區段自動進行代碼分割。這意味著導航時只會載入當前路由所需的代碼。

2. 預取 (Prefetching)

預取是一種在使用者訪問路由前,在背景預先載入路由的方式。

Next.js 中有兩種預取路由的方式:

  • <Link> 元件:當路由出現在使用者的視野中時,會自動進行預取。預取發生在頁面首次載入或通過滾動進入視野時。
  • router.prefetch():可以使用 useRouter 鉤子以程式方式預取路由。

<Link> 的預設預取行為(即當 prefetch 屬性未指定或設為 null 時)會根據您是否使用 loading.js 而有所不同。只有共享的佈局,直到第一個 loading.js 檔案為止的元件「樹」會被預取並快取 30 秒。這減少了獲取整個動態路由的成本,意味著您可以顯示 即時載入狀態,為使用者提供更好的視覺反饋。

您可以通過將 prefetch 屬性設為 false 來禁用預取。或者,您可以通過將 prefetch 屬性設為 true 來預取超出載入邊界的完整頁面數據。

更多資訊請參閱 <Link> API 參考文件

重要須知:

  • 預取功能在開發環境中不會啟用,僅在生產環境中有效。

3. 快取 (Caching)

Next.js 有一個稱為 路由器快取 (Router Cache)記憶體客戶端快取。當使用者在應用程式中導航時,預取 的路由區段和已訪問路由的 React 伺服器元件負載 (Payload) 會被儲存在快取中。

這意味著在導航時,會盡可能重用快取,而不是向伺服器發送新請求,從而通過減少請求和傳輸的數據量來提高效能。

了解更多關於 路由器快取 的運作方式和如何配置它。

4. 部分渲染 (Partial Rendering)

部分渲染意味著在導航時,只有變更的路由區段會在客戶端重新渲染,任何共享的區段都會被保留。

例如,在導航兩個兄弟路由 /dashboard/settings/dashboard/analytics 之間時,settings 頁面會被卸載,analytics 頁面會以新狀態掛載,而共享的 dashboard 佈局會被保留。這種行為也存在於同一動態區段的兩個路由之間,例如 /blog/[slug]/page 和從 /blog/first 導航到 /blog/second

部分渲染的運作方式

如果沒有部分渲染,每次導航都會導致整個頁面在客戶端重新渲染。只渲染變更的區段減少了傳輸的數據量和執行時間,從而提高效能。

5. 軟導航 (Soft Navigation)

瀏覽器在頁面之間導航時會執行「硬導航」。Next.js 應用程式路由器實現了頁面之間的「軟導航」,確保只有變更的路由區段會重新渲染(部分渲染)。這使得客戶端 React 狀態在導航期間得以保留。

6. 前進和後退導航

預設情況下,Next.js 會為前進和後退導航保持滾動位置,並重用 路由器快取 中的路由區段。

7. pages/app/ 之間的路由

當從 pages/ 逐步遷移到 app/ 時,Next.js 路由器會自動處理兩者之間的硬導航。為了檢測從 pages/app/ 的轉換,有一個客戶端路由器過濾器會利用應用路由的機率性檢查,這偶爾會導致誤判。預設情況下,這種情況應該非常罕見,因為我們將誤判機率配置為 0.01%。這個機率可以通過 next.config.js 中的 experimental.clientRouterFilterAllowedRate 選項進行自訂。需要注意的是,降低誤判率會增加客戶端套件中生成的過濾器的大小。

或者,如果您希望完全禁用此處理並手動管理 pages/app/ 之間的路由,可以在 next.config.js 中將 experimental.clientRouterFilter 設為 false。當此功能被禁用時,任何與應用路由重疊的頁面動態路由預設將無法正確導航。