伺服器動作 (Server Actions) 與資料異動 (Mutations)

伺服器動作 (Server Actions) 是在伺服器上執行的非同步函式。它們可以在伺服器與客戶端元件中呼叫,用於處理 Next.js 應用程式中的表單提交與資料異動。

🎥 觀看影片: 深入了解使用伺服器動作進行資料異動 → YouTube (10 分鐘)

慣例

伺服器動作可以使用 React 的 "use server" 指令來定義。您可以將指令放在 async 函式頂部以標記該函式為伺服器動作,或放在獨立檔案頂部以標記該檔案所有匯出為伺服器動作。

伺服器元件

伺服器元件可以使用函式層級或模組層級的 "use server" 指令。若要內嵌伺服器動作,請在函式主體頂部添加 "use server"

export default function Page() {
  // 伺服器動作
  async function create() {
    'use server'
    // 異動資料
  }

  return '...'
}

客戶端元件

要在客戶端元件中呼叫伺服器函式,請建立新檔案並在其頂部添加 "use server" 指令。檔案內所有匯出的函式將被標記為伺服器函式,可在客戶端與伺服器元件中重複使用:

'use server'

export async function create() {}

將動作作為屬性傳遞

您也可以將伺服器動作作為屬性傳遞給客戶端元件:

<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 結尾的屬性被假定接收伺服器動作。這只是一個啟發式方法,因為 TypeScript 插件實際上不知道它接收的是伺服器動作還是普通函式。執行時類型檢查仍會確保您不會意外將函式傳遞給客戶端元件。

行為

  • 伺服器動作可以使用 <form> 元素中的 action 屬性來呼叫。
    • 伺服器元件預設支援漸進增強,意味著即使 JavaScript 尚未載入或已停用,表單仍會提交。
    • 在客戶端元件中,呼叫伺服器動作的表單會在 JavaScript 尚未載入時排隊提交,優先考慮客戶端水合 (hydration)。
    • 水合後,瀏覽器不會在表單提交時重新整理。
  • 伺服器動作不限於 <form>,可以從事件處理程序、useEffect、第三方函式庫和其他表單元素(如 <button>)呼叫。
  • 伺服器動作與 Next.js 快取和重新驗證架構整合。當動作被呼叫時,Next.js 可以在單次伺服器往返中返回更新的 UI 和新資料。
  • 在幕後,動作使用 POST 方法,只有此 HTTP 方法可以呼叫它們。
  • 伺服器動作的參數和返回值必須可由 React 序列化。請參閱 React 文件了解可序列化的參數和值
  • 伺服器動作是函式,這意味著它們可以在應用程式的任何地方重複使用。
  • 伺服器動作繼承它們所使用頁面或佈局的執行環境。
  • 伺服器動作繼承它們所使用頁面或佈局的路由區段配置,包括 maxDuration 等欄位。

範例

事件處理程序

雖然通常在 <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>總按讚數: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        按讚
      </button>
    </>
  )
}

您也可以為表單元素添加事件處理程序,例如在 onChange 時儲存表單欄位:

app/ui/edit-post.tsx
'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">發布</button>
    </form>
  )
}

對於這種情況,當可能快速連續觸發多個事件時,我們建議使用防抖 (debouncing) 來避免不必要的伺服器動作呼叫。

useEffect

您可以使用 React 的 useEffect 鉤子在元件掛載或依賴項變更時呼叫伺服器動作。這對於依賴全域事件或需要自動觸發的異動很有用。例如,onKeyDown 用於應用程式快捷鍵、無限滾動的交集觀察器鉤子,或在元件掛載時更新檢視計數:

'use client'

import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // 您可以使用 `isPending` 向用戶提供反饋
  return <p>總檢視次數: {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') // 更新快取文章
  redirect(`/post/${id}`) // 導向新文章頁面
}

Cookies

您可以在伺服器動作中使用 cookies API 來 getsetdelete cookies:

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  const cookieStore = await cookies()

  // 取得 cookie
  cookieStore.get('name')?.value

  // 設定 cookie
  cookieStore.set('name', 'Delba')

  // 刪除 cookie
  cookieStore.delete('name')
}

請參閱其他範例了解如何從伺服器動作刪除 cookies。

安全性

預設情況下,當建立並匯出伺服器動作時,它會建立一個公開的 HTTP 端點,應以相同的安全假設和授權檢查來對待。這意味著,即使伺服器動作或工具函式未在程式碼的其他地方匯入,它仍然是公開可存取的。

為了提高安全性,Next.js 具有以下內建功能:

  • 安全的動作 ID: Next.js 建立加密、非確定性的 ID,允許客戶端引用和呼叫伺服器動作。這些 ID 在構建之間定期重新計算以增強安全性。
  • 死碼消除: 未使用的伺服器動作(由其 ID 引用)會從客戶端套件中移除,以避免第三方公開存取。

須知:

ID 在編譯期間建立,並快取最多 14 天。當啟動新構建或構建快取失效時,它們將重新生成。 此安全改進減少了缺少身份驗證層情況下的風險。然而,您仍應將伺服器動作視為公開 HTTP 端點。

// app/actions.js
'use server'

// 此動作**在**我們的應用程式中使用,因此 Next.js
// 將建立安全 ID 以允許客戶端引用
// 和呼叫伺服器動作。
export async function updateUserAction(formData) {}

// 此動作**未在**我們的應用程式中使用,因此 Next.js
// 將在 `next build` 期間自動移除此程式碼
// 並且不會建立公開端點。
export async function deleteUserAction(formData) {}

身份驗證與授權

您應確保用戶有權執行該動作。例如:

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('您必須登入才能執行此動作')
  }

  // ...
}

閉包與加密

在元件內定義伺服器動作會建立一個閉包 (closure),其中動作可以存取外部函式的範圍。例如,publish 動作可以存取 publishVersion 變數:

export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('自按下發布以來版本已變更');
    }
    ...
  }

  return (
    <form>
      <button formAction={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 應用程式會自動處理此問題。

允許的來源 (進階)

由於伺服器動作 (Server Actions) 可以在 <form> 元素中觸發,這使其可能遭受 CSRF 攻擊

在底層,伺服器動作使用 POST 方法,且僅允許此 HTTP 方法觸發它們。這在現代瀏覽器中可防止大多數 CSRF 漏洞,特別是當 SameSite cookies 成為預設值時。

作為額外保護,Next.js 中的伺服器動作還會比對 Origin 標頭Host 標頭 (或 X-Forwarded-Host)。若兩者不符,請求將被中止。換言之,伺服器動作只能在與宿主頁面相同的主機上觸發。

對於使用反向代理或多層後端架構的大型應用程式 (伺服器 API 與生產網域不同),建議使用 serverActions.allowedOrigins 配置選項來指定安全來源清單。此選項接受字串陣列。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

了解更多關於 安全性與伺服器動作 的資訊。

延伸資源

更多資訊請參考以下 React 文件:

On this page