如何使用 Next.js 建構單頁應用程式 (SPA)

Next.js 完整支援建構單頁應用程式 (Single-Page Applications, SPA)。

這包括透過預取實現快速路由轉換、客戶端資料獲取、使用瀏覽器 API、整合第三方客戶端函式庫、建立靜態路由等功能。

如果您已有現成的 SPA,可以遷移至 Next.js 而無需大幅修改程式碼。Next.js 允許您根據需求逐步添加伺服器功能。

什麼是單頁應用程式?

SPA 的定義各有不同。我們將「嚴格 SPA」定義為:

  • 客戶端渲染 (CSR):應用程式由單一 HTML 檔案 (例如 index.html) 提供服務。所有路由、頁面轉換和資料獲取均由瀏覽器中的 JavaScript 處理。
  • 無整頁重新載入:與為每個路由請求新文件不同,客戶端 JavaScript 會操作當前頁面的 DOM 並按需獲取資料。

嚴格 SPA 通常需要載入大量 JavaScript 才能使頁面具有互動性。此外,客戶端資料瀑布流可能難以管理。使用 Next.js 建構 SPA 可以解決這些問題。

為什麼選擇 Next.js 建構 SPA?

Next.js 可以自動進行 JavaScript 套件程式碼分割,並為不同路由生成多個 HTML 進入點。這避免了在客戶端載入不必要的 JavaScript 程式碼,減少了套件大小並實現更快的頁面載入。

next/link 元件會自動預取路由,提供嚴格 SPA 的快速頁面轉換,同時具有將應用程式路由狀態持久化到 URL 以便連結和分享的優勢。

Next.js 可以從靜態網站甚至嚴格 SPA (所有內容都在客戶端渲染) 開始。如果專案成長,Next.js 允許您根據需求逐步添加更多伺服器功能 (例如 React 伺服器元件伺服器操作 等)。

範例

讓我們探討建構 SPA 的常見模式以及 Next.js 如何解決這些問題。

在 Context Provider 中使用 React 的 use

我們建議在父元件 (或佈局) 中獲取資料,返回 Promise,然後在客戶端元件中使用 React 的 use 鉤子 解包值。

Next.js 可以在伺服器上提前開始資料獲取。在此範例中,這是根佈局 — 應用程式的進入點。伺服器可以立即開始將回應串流至客戶端。

通過將資料獲取「提升」到根佈局,Next.js 會在應用程式中任何其他元件之前,提前在伺服器上啟動指定的請求。這消除了客戶端瀑布流,並避免了客戶端和伺服器之間的多個往返。它還可以顯著提高效能,因為您的伺服器更接近 (理想情況下與) 資料庫位於同一位置。

例如,更新您的根佈局以呼叫 Promise,但 不要 等待它。

import { UserProvider } from './user-provider'
import { getUser } from './user' // 某個伺服器端函式

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  let userPromise = getUser() // 不要 await

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}

雖然您可以延遲並傳遞單一 Promise 作為屬性給客戶端元件,但我們通常會看到此模式與 React context provider 配對使用。這使得透過自訂 React 鉤子從客戶端元件更容易存取。

您可以將 Promise 轉發給 React context provider:

'use client';

import { createContext, useContext, ReactNode } from 'react';

type User = any;
type UserContextType = {
  userPromise: Promise<User | null>;
};

const UserContext = createContext<UserContextType | null>(null);

export function useUser(): UserContextType {
  let context = useContext(UserContext);
  if (context === null) {
    throw new Error('useUser 必須在 UserProvider 內使用');
  }
  return context;
}

export function UserProvider({
  children,
  userPromise
}: {
  children: ReactNode;
  userPromise: Promise<User | null>;
}) {
  return (
    <UserContext.Provider value={{ userPromise }}>
      {children}
    </UserContext.Provider>
  );
}

最後,您可以在任何客戶端元件中呼叫 useUser() 自訂鉤子並解包 Promise:

'use client'

import { use } from 'react'
import { useUser } from './user-provider'

export function Profile() {
  const { userPromise } = useUser()
  const user = use(userPromise)

  return '...'
}

使用 Promise 的元件 (例如上面的 Profile) 將會被暫停。這實現了部分水合 (hydration)。您可以在 JavaScript 完成載入之前看到串流和預渲染的 HTML。

使用 SWR 的 SPA

SWR 是一個流行的 React 資料獲取函式庫。

使用 SWR 2.3.0 (和 React 19+),您可以逐步採用伺服器功能,同時與現有的基於 SWR 的客戶端資料獲取程式碼並存。這是上述 use() 模式的抽象。這意味著您可以在客戶端和伺服器端之間移動資料獲取,或同時使用兩者:

  • 僅客戶端useSWR(key, fetcher)
  • 僅伺服器useSWR(key) + RSC 提供的資料
  • 混合useSWR(key, fetcher) + RSC 提供的資料

例如,使用 <SWRConfig>fallback 包裝您的應用程式:

import { SWRConfig } from 'swr'
import { getUser } from './user' // 某個伺服器端函式

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // 這裡我們不 await getUser()
          // 只有讀取此資料的元件會暫停
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}

由於這是伺服器元件,getUser() 可以安全地讀取 cookies、headers 或與您的資料庫通訊。不需要單獨的 API 路由。<SWRConfig> 下方的客戶端元件可以使用相同的鍵呼叫 useSWR() 來檢索使用者資料。帶有 useSWR 的元件程式碼 不需要任何更改 從您現有的客戶端獲取解決方案。

'use client'

import useSWR from 'swr'

export function Profile() {
  const fetcher = (url) => fetch(url).then((res) => res.json())
  // 您已經熟悉的相同 SWR 模式
  const { data, error } = useSWR('/api/user', fetcher)

  return '...'
}

fallback 資料可以預渲染並包含在初始 HTML 回應中,然後在子元件中使用 useSWR 立即讀取。SWR 的輪詢、重新驗證和快取仍然 僅在客戶端執行,因此它保留了 SPA 所需的所有互動性。

由於初始 fallback 資料由 Next.js 自動處理,您現在可以刪除以前需要檢查 data 是否為 undefined 的任何條件邏輯。當資料載入時,最接近的 <Suspense> 邊界將會暫停。

SWRRSCRSC + SWR
SSR 資料Cross IconCheck IconCheck Icon
SSR 串流Cross IconCheck IconCheck Icon
重複請求去重Check IconCheck IconCheck Icon
客戶端功能Check IconCross IconCheck Icon

使用 React Query 的 SPA

您可以在客戶端和伺服器上使用 React Query 與 Next.js。這使您能夠建構嚴格 SPA,同時利用 Next.js 中的伺服器功能與 React Query 配對。

React Query 文件 中了解更多。

僅在瀏覽器中渲染元件

客戶端元件在 next build 期間會進行預渲染。如果您想禁用客戶端元件的預渲染並僅在瀏覽器環境中載入它,可以使用 next/dynamic

import dynamic from 'next/dynamic'

const ClientOnlyComponent = dynamic(() => import('./component'), {
  ssr: false,
})

這對於依賴於 windowdocument 等瀏覽器 API 的第三方函式庫很有用。您還可以添加一個 useEffect 來檢查這些 API 是否存在,如果不存在,則返回 null 或將被預渲染的載入狀態。

客戶端的淺層路由

如果您從 Create React AppVite 等嚴格 SPA 遷移,您可能有現有程式碼使用淺層路由來更新 URL 狀態。這對於在應用程式中 不使用 預設 Next.js 檔案系統路由的情況下,手動轉換視圖很有用。

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

pushStatereplaceState 呼叫會整合到 Next.js 路由器中,允許您與 usePathnameuseSearchParams 同步。

'use client'

import { useSearchParams } from 'next/navigation'

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

  function updateSorting(sortOrder: string) {
    const urlSearchParams = new URLSearchParams(searchParams.toString())
    urlSearchParams.set('sort', sortOrder)
    window.history.pushState(null, '', `?${urlSearchParams.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 urlSearchParams = new URLSearchParams(searchParams.toString())
    urlSearchParams.set('sort', sortOrder)
    window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
  }

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

了解更多關於 Next.js 中路由和導航的工作原理。

在客戶端元件中使用伺服器操作

您可以逐步採用伺服器操作,同時仍使用客戶端元件。這使您可以移除呼叫 API 路由的樣板程式碼,轉而使用 React 功能如 useActionState 來處理載入和錯誤狀態。

例如,建立您的第一個伺服器操作:

'use server'

export async function create() {}

您可以從客戶端導入和使用伺服器操作,類似於呼叫 JavaScript 函式。您不需要手動建立 API 端點:

'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>建立</button>
}

了解更多關於使用伺服器操作變更資料

靜態匯出 (選用)

Next.js 還支援生成完全靜態網站。這比嚴格 SPA 有一些優勢:

  • 自動程式碼分割:Next.js 會為每個路由生成一個 HTML 檔案,而不是提供單一 index.html,因此您的訪客無需等待客戶端 JavaScript 套件即可更快獲得內容。
  • 改善使用者體驗:您會獲得每個路由的完整渲染頁面,而不是所有路由的最小骨架。當用戶在客戶端導航時,轉換仍然即時且類似 SPA。

要啟用靜態匯出,請更新您的配置:

next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export',
}

export default nextConfig

執行 next build 後,Next.js 將為您的應用程式建立一個包含 HTML/CSS/JS 資源的 out 資料夾。

注意:靜態匯出不支援 Next.js 伺服器功能。了解更多

將現有專案遷移至 Next.js

您可以按照我們的指南逐步遷移至 Next.js:

如果您已經在使用 Pages Router 的 SPA,可以學習如何逐步採用 App Router