如何使用 Next.js 構建單頁應用程式
Next.js 完全支援構建單頁應用程式 (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 hook 在客戶端元件中解包該值。
Next.js 可以及早在伺服器上開始資料獲取。在此示例中,它是根佈局——您應用程式的入口點。伺服器可以立即開始向客戶端流式傳輸響應。
透過將資料獲取“提升”到根佈局,Next.js 會在應用程式中的任何其他元件之前及早啟動伺服器上指定的請求。這消除了客戶端瀑布並防止了客戶端和伺服器之間的多次往返。它還可以顯著提高效能,因為您的伺服器更靠近(並且理想情況下與)您的資料庫所在的位置。
例如,更新您的根佈局以呼叫 Promise,但**不要**等待它。
import { UserProvider } from './user-provider'
import { getUser } from './user' // some server-side function
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // do NOT await
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}雖然您可以推遲並傳遞單個 Promise 作為 props 給客戶端元件,但我們通常會看到這種模式與 React context provider 配對使用。這使得客戶端元件使用自定義 React Hook 能夠更輕鬆地訪問。
您可以將 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 must be used within a UserProvider');
}
return context;
}
export function UserProvider({
children,
userPromise
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
);
}最後,您可以在任何客戶端元件中呼叫 useUser() 自定義 Hook 並解包 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)將被掛起。這使得部分水合成為可能。您可以在 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' // some server-side function
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// We do NOT await getUser() here
// Only components that read this data will suspend
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}因為這是一個伺服器元件,getUser() 可以安全地讀取 cookies、headers 或與您的資料庫通訊。不需要單獨的 API 路由。<SWRConfig> 下的客戶端元件可以使用相同的 key 呼叫 useSWR() 來檢索使用者資料。使用 useSWR 的元件程式碼**不需要對您現有的客戶端獲取解決方案進行任何更改**。
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// The same SWR pattern you already know
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,
})這對於依賴 window 或 document 等瀏覽器 API 的第三方庫可能很有用。您還可以新增一個 useEffect 來檢查這些 API 是否存在,如果不存在,則返回 null 或載入狀態(將被預渲染)。
客戶端淺路由
如果您正在從像 Create React App 或 Vite 這樣的嚴格 SPA 遷移,您可能已經有淺路由程式碼來更新 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')}>Sort Ascending</button>
<button onClick={() => updateSorting('desc')}>Sort Descending</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()}>Create</button>
}瞭解有關使用伺服器操作修改資料的更多資訊。
靜態匯出(可選)
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。
這有幫助嗎?