伺服器行為與變更
伺服器行為 是在伺服器上執行的非同步函式。它們可以在伺服器和用戶端元件中呼叫,以處理 Next.js 應用程式中的表單提交和資料變更。
🎥 觀看: 深入了解伺服器行為的變更 → YouTube (10 分鐘)。
慣例
伺服器行為可以使用 React "use server"
指示詞來定義。您可以將指示詞放在 async
函式的頂部,以將該函式標記為伺服器行為,或放在個別檔案的頂部,以將該檔案的所有匯出標記為伺服器行為。
伺服器元件
伺服器元件可以使用行內函式層級或模組層級的 "use server"
指示詞。若要內嵌伺服器行為,請將 "use server"
新增至函式主體的頂部
export default function Page() {
// Server Action
async function create() {
'use server'
// Mutate data
}
return '...'
}
用戶端元件
若要在用戶端元件中呼叫伺服器行為,請建立新檔案,並將 "use server"
指示詞新增至檔案的頂部。檔案內所有匯出的函式都將標記為伺服器行為,這些行為可以在用戶端和伺服器元件中重複使用
'use server'
export async function create() {}
'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>Create</button>
}
將行為作為 props 傳遞
您也可以將伺服器行為作為 prop 傳遞至用戶端元件
<ClientComponent updateItemAction={updateItem} />
'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}>{/* ... */}</form>
}
通常,Next.js TypeScript 外掛程式會標記 client-component.tsx
中的 updateItemAction
,因為它是一個函式,通常無法在用戶端-伺服器邊界之間序列化。但是,名稱為 action
或以 Action
結尾的 props 會被假定為接收伺服器行為。這只是一種啟發式方法,因為 TypeScript 外掛程式實際上不知道它是否接收到伺服器行為或普通函式。執行階段類型檢查仍將確保您不會意外地將函式傳遞至用戶端元件。
行為
- 伺服器行為可以使用
<form>
元素中的action
屬性來調用- 伺服器元件預設支援漸進式增強,這表示即使 JavaScript 尚未載入或已停用,表單仍會提交。
- 在用戶端元件中,調用伺服器行為的表單如果 JavaScript 尚未載入,將會將提交排入佇列,優先處理用戶端 hydration。
- hydration 後,瀏覽器不會在表單提交時重新整理。
- 伺服器行為不限於
<form>
,可以從事件處理器、useEffect
、第三方函式庫和其他表單元素 (如<button>
) 調用。 - 伺服器行為與 Next.js 快取和重新驗證 架構整合。當調用行為時,Next.js 可以在單個伺服器往返中傳回更新的 UI 和新資料。
- 在幕後,行為使用
POST
方法,且只有此 HTTP 方法可以調用它們。 - 伺服器行為的引數和傳回值必須可由 React 序列化。請參閱 React 文件,以取得 可序列化的引數和值的清單。
- 伺服器行為是函式。這表示它們可以在應用程式中的任何位置重複使用。
- 伺服器行為繼承使用它們的頁面或版面配置的 執行階段。
- 伺服器行為繼承使用它們的頁面或版面配置的 路由區段設定,包括
maxDuration
等欄位。
範例
表單
React 擴展了 HTML <form>
元素,以允許使用 action
prop 調用伺服器行為。
在表單中調用時,行為會自動接收 FormData
物件。您不需要使用 React useState
來管理欄位,而是可以使用原生的 FormData
方法 來擷取資料
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}
要知道的好資訊
- 範例:具有載入和錯誤狀態的表單
- 當處理具有許多欄位的表單時,您可能會想要考慮使用
entries()
方法搭配 JavaScript 的Object.fromEntries()
。例如:const rawFormData = Object.fromEntries(formData)
。需要注意的一件事是,formData
將包含額外的$ACTION_
屬性。- 請參閱 React
<form>
文件以了解更多資訊。
傳遞額外參數
您可以使用 JavaScript bind
方法將額外引數傳遞至伺服器行為。
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
除了表單資料外,伺服器行為還將接收 userId
引數
'use server'
export async function updateUser(userId: string, formData: FormData) {}
要知道的好資訊:
- 另一種方法是將引數作為表單中的隱藏輸入欄位傳遞 (例如
<input type="hidden" name="userId" value={userId} />
)。但是,該值將成為渲染 HTML 的一部分,並且不會被編碼。.bind
在伺服器和用戶端元件中都有效。它也支援漸進式增強。
巢狀表單元素
您也可以在巢狀於 <form>
內的元素 (例如 <button>
、<input type="submit">
和 <input type="image">
) 中調用伺服器行為。這些元素接受 formAction
prop 或 事件處理器。
如果您想要在表單中呼叫多個伺服器行為,這會很有用。例如,您可以建立一個特定的 <button>
元素,以在發佈文章之外,額外儲存文章草稿。請參閱 React <form>
文件以取得更多資訊。
程式化表單提交
您可以使用 requestSubmit()
方法以程式化方式觸發表單提交。例如,當使用者使用 ⌘
+ Enter
鍵盤快捷鍵提交表單時,您可以監聽 onKeyDown
事件
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
這將觸發最近的 <form>
祖先的提交,這將調用伺服器行為。
伺服器端表單驗證
您可以使用 HTML 屬性 (例如 required
和 type="email"
) 進行基本用戶端表單驗證。
對於更進階的伺服器端驗證,您可以使用 zod 等函式庫,以在變更資料之前驗證表單欄位
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
在伺服器上驗證欄位後,您可以傳回伺服器行為中可序列化的物件,並使用 React useActionState
Hook 向使用者顯示訊息。
- 透過將行為傳遞至
useActionState
,行為的函式簽名會變更為接收新的prevState
或initialState
參數作為其第一個引數。 useActionState
是 React Hook,因此必須在用戶端元件中使用。
'use server'
import { redirect } from 'next/navigation'
export async function createUser(prevState: any, formData: FormData) {
const res = await fetch('https://...')
const json = await res.json()
if (!res.ok) {
return { message: 'Please enter a valid email' }
}
redirect('/dashboard')
}
然後,您可以將您的行為傳遞至 useActionState
Hook,並使用傳回的 state
來顯示錯誤訊息。
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Sign up</button>
</form>
)
}
待處理狀態
useActionState
Hook 公開了一個 pending
布林值,可用於在執行行為時顯示載入指示器。
或者,您可以使用 useFormStatus
Hook,以在執行行為時顯示載入指示器。當使用此 Hook 時,您需要建立一個單獨的元件來渲染載入指示器。例如,在行為待處理時停用按鈕
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
然後,您可以將 SubmitButton
元件巢狀於表單內
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Other form elements */}
<SubmitButton />
</form>
)
}
要知道的好資訊: 在 React 19 中,
useFormStatus
在傳回的物件上包含其他索引鍵,例如 data、method 和 action。如果您未使用 React 19,則只有pending
索引鍵可用。
樂觀更新
您可以使用 React useOptimistic
Hook 在伺服器行為完成執行之前樂觀地更新 UI,而不是等待回應
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...state, { message: newMessage }])
const formAction = async (formData: FormData) => {
const message = formData.get('message') as string
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
事件處理器
雖然在 <form>
元素中使用伺服器行為很常見,但也可以使用事件處理器 (例如 onClick
) 調用它們。例如,增加讚數
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}
您也可以將事件處理器新增至表單元素,例如,將表單欄位儲存 onChange
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publish</button>
</form>
)
}
對於這種情況,可能會快速連續觸發多個事件,我們建議進行防抖,以防止不必要的伺服器行為調用。
useEffect
您可以使用 React useEffect
Hook 在元件掛載或依賴項變更時調用伺服器行為。這對於依賴全域事件或需要自動觸發的變更很有用。例如,應用程式快捷鍵的 onKeyDown
、無限捲動的 intersection observer Hook,或在元件掛載時更新檢視計數
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views: {views}</p>
}
請記住考慮 useEffect
的 行為和注意事項。
錯誤處理
當拋出錯誤時,它將被用戶端上最近的 error.js
或 <Suspense>
邊界捕獲。請參閱 錯誤處理 以取得更多資訊。
要知道的好資訊
- 除了拋出錯誤之外,您還可以傳回物件以由
useActionState
處理。請參閱 伺服器端驗證和錯誤處理。
重新驗證資料
您可以使用 revalidatePath
API 在伺服器行為內重新驗證 Next.js 快取
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
}
或者使用快取標籤和 revalidateTag
使特定資料擷取失效
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
}
重新導向
如果您希望在伺服器行為完成後將使用者重新導向到不同的路由,您可以使用 redirect
API。redirect
需要在 try/catch
區塊之外呼叫
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
Cookie (Cookies)
您可以使用 cookies
API,在伺服器行為 (Server Action) 內 get
、set
和 delete
Cookie。
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
const cookieStore = await cookies()
// Get cookie
cookieStore.get('name')?.value
// Set cookie
cookieStore.set('name', 'Delba')
// Delete cookie
cookieStore.delete('name')
}
請參閱其他範例,了解如何從伺服器行為中刪除 Cookie。
安全性
預設情況下,當建立並匯出伺服器行為時,它會建立一個公開的 HTTP 端點,應以相同的安全性假設和授權檢查來對待。這表示,即使伺服器行為或實用工具函式未在程式碼的其他地方匯入,它仍然可以公開存取。
為了提高安全性,Next.js 具有以下內建功能
- 安全行為 ID: Next.js 建立加密、非決定性的 ID,以允許用戶端參考和呼叫伺服器行為。這些 ID 會在建置之間定期重新計算,以增強安全性。
- 無效程式碼移除: 未使用的伺服器行為 (透過其 ID 參考) 會從用戶端套件中移除,以避免第三方公開存取。
要知道的好資訊:
ID 在編譯期間建立,並快取最多 14 天。當啟動新的建置或建置快取失效時,它們將會重新產生。此安全性改進降低了缺少身分驗證層的風險。但是,您仍然應該將伺服器行為視為公開的 HTTP 端點。
// app/actions.js
'use server'
// This action **is** used in our application, so Next.js
// will create a secure ID to allow the client to reference
// and call the Server Action.
export async function updateUserAction(formData) {}
// This action **is not** used in our application, so Next.js
// will automatically remove this code during `next build`
// and will not create a public endpoint.
export async function deleteUserAction(formData) {}
身分驗證與授權
您應確保使用者已獲得授權執行操作。例如
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}
閉包與加密
在元件內定義伺服器行為會建立閉包,其中行為可以存取外部函式的範圍。例如,publish
行為可以存取 publishVersion
變數
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return (
<form>
<button formAction={publish}>Publish</button>
</form>
);
}
當您需要在渲染時擷取資料的快照 (例如 publishVersion
),以便在稍後呼叫行為時使用時,閉包非常有用。
但是,為了實現這一點,擷取的變數會傳送到用戶端,並在呼叫行為時傳回伺服器。為了防止敏感資料暴露給用戶端,Next.js 會自動加密封閉的變數。每次建置 Next.js 應用程式時,都會為每個行為產生一個新的私密金鑰。這表示行為只能針對特定的建置呼叫。
小知識: 我們不建議僅依賴加密來防止敏感值暴露在用戶端。相反地,您應該使用 React taint API 來主動防止特定資料傳送到用戶端。
覆寫加密金鑰 (進階)
當跨多個伺服器自行託管 Next.js 應用程式時,每個伺服器執行個體最終可能會使用不同的加密金鑰,導致潛在的不一致性。
為了減輕這種情況,您可以使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
環境變數覆寫加密金鑰。指定此變數可確保您的加密金鑰在建置之間保持持久性,且所有伺服器執行個體都使用相同的金鑰。此變數必須經過 AES-GCM 加密。
這是一個進階的使用案例,其中跨多個部署的一致加密行為對於您的應用程式至關重要。您應考慮金鑰輪換和簽署等標準安全性實務。
小知識: 部署到 Vercel 的 Next.js 應用程式會自動處理此問題。
允許的來源 (進階)
由於伺服器行為可以在 <form>
元素中呼叫,這會使其容易受到 CSRF 攻擊。
在幕後,伺服器行為使用 POST
方法,且僅允許此 HTTP 方法呼叫它們。這可以防止現代瀏覽器中的大多數 CSRF 漏洞,尤其是 SameSite Cookie 作為預設值的情況下。
作為額外的保護,Next.js 中的伺服器行為也會比較 Origin 標頭 與 Host 標頭 (或 X-Forwarded-Host
)。如果這些不符,則請求將會中止。換句話說,伺服器行為只能在與託管它的頁面相同的主機上呼叫。
對於使用反向 Proxy 或多層後端架構 (其中伺服器 API 與生產網域不同) 的大型應用程式,建議使用組態選項 serverActions.allowedOrigins
選項來指定安全來源的清單。此選項接受字串陣列。
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
深入了解安全性與伺服器行為。
其他資源
如需更多資訊,請查看以下 React 文件
這對您有幫助嗎?