如何使用 Next.js 建構單頁應用程式 (SPA)
Next.js 完全支援建構單頁應用程式 (Single-Page Applications, SPA)。
這包括透過預取實現快速路由轉換、客戶端資料獲取、使用瀏覽器 API、與第三方客戶端庫整合、建立靜態路由等功能。
如果您已有現有的 SPA,可以遷移到 Next.js 而無需大幅修改程式碼。Next.js 允許您根據需求逐步添加伺服器功能。
什麼是單頁應用程式?
單頁應用程式的定義各有不同。我們將「嚴格 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>
)
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // 某些伺服器端函數
export default function RootLayout({ children }) {
let userPromise = getUser() // 不要 await
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
雖然您可以延遲並傳遞單個 Promise 作為 prop 給客戶端元件,但我們通常看到此模式與 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>
);
}
'use client'
import { createContext, useContext, ReactNode } from 'react'
const UserContext = createContext(null)
export function useUser() {
let context = useContext(UserContext)
if (context === null) {
throw new Error('useUser 必須在 UserProvider 內使用')
}
return context
}
export function UserProvider({ children, userPromise }) {
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 '...'
}
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
消費 Promise 的元件(例如上面的 Profile
)將被暫停。這實現了部分水合。您可以在 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>
)
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // 某些伺服器端函數
export default function RootLayout({ children }) {
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 '...'
}
'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>
邊界將被暫停。
SWR | RSC | RSC + SWR | |
---|---|---|---|
SSR 資料 | |||
SSR 時串流 | |||
請求去重 | |||
客戶端功能 |
使用 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,
})
這對於依賴瀏覽器 API(如 window
或 document
)的第三方庫很有用。您還可以添加一個 useEffect
來檢查這些 API 是否存在,如果它們不存在,則返回 null
或預渲染的載入狀態。
客戶端的淺層路由
如果您從嚴格 SPA(如 Create React App 或 Vite 遷移,您可能有現有的代碼使用淺層路由來更新 URL 狀態。這對於不使用預設 Next.js 文件系統路由的應用程式中手動轉換視圖很有用。
Next.js 允許您使用原生 window.history.pushState
和 window.history.replaceState
方法來更新瀏覽器的歷史堆疊而無需重新載入頁面。
pushState
和 replaceState
呼叫會整合到 Next.js 路由器中,允許您與 usePathname
和 useSearchParams
同步。
'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() {}
'use server'
export async function create() {}
您可以從客戶端導入和使用伺服器操作,類似於呼叫 JavaScript 函數。您不需要手動建立 API 端點:
了解更多關於使用伺服器操作變更資料。
靜態導出(可選)
Next.js 還支援生成完全靜態網站。這比嚴格 SPA 有一些優勢:
- 自動程式碼分割:Next.js 將為每個路由生成一個 HTML 檔案,而不是發送單個
index.html
,因此您的訪問者無需等待客戶端 JavaScript 套件即可更快獲取內容。 - 改進的使用者體驗:您獲得每個路由的完全渲染頁面,而不是所有路由的最小骨架。當用戶在客戶端導航時,轉換保持即時且類似 SPA。
要啟用靜態導出,請更新您的配置:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export',
}
export default nextConfig
運行 next build
後,Next.js 將建立一個 out
資料夾,其中包含應用程式的 HTML/CSS/JS 資源。
注意:Next.js 伺服器功能不支援靜態導出。了解更多。
將現有專案遷移到 Next.js
您可以按照我們的指南逐步遷移到 Next.js:
如果您已經在使用 Pages Router 的 SPA,可以學習如何逐步採用 App Router。