动态

详情 返回 返回

SvelteKit 最新中文文檔教程(4)—— 表單 actions - 动态 详情

前言

Svelte,一個語法簡潔、入門容易,面向未來的前端框架。

從 Svelte 誕生之初,就備受開發者的喜愛,根據統計,從 2019 年到 2024 年,連續 6 年一直是開發者最感興趣的前端框架 No.1

image.png

Svelte 以其獨特的編譯時優化機制著稱,具有輕量級高性能易上手等特性,非常適合構建輕量級 Web 項目

為了幫助大家學習 Svelte,我同時搭建了 Svelte 最新的中文文檔站點。

如果需要進階學習,也可以入手我的小冊《Svelte 開發指南》,語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

表單 actions

+page.server.js 文件可以導出 actions,允許您使用 <form> 元素向服務端 POST 數據。

使用 <form> 時,客户端 JavaScript 是可選的,但您可以輕鬆地使用 JavaScript 漸進式增強 表單交互,以提供最佳的用户體驗。

默認 action

在最簡單的情況下,一個頁面聲明一個 default action:

/// file: src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
    default: async (event) => {
        // TODO log the user in
    }
};

要從 /login 頁面調用此 action,只需添加一個 <form> —— 不需要 JavaScript:

<!--- file: src/routes/login/+page.svelte --->
<form method="POST">
  <label>
    Email
    <input name="email" type="email">
  </label>
  <label>
    Password
    <input name="password" type="password">
  </label>
  <button>Log in</button>
</form>

如果有人點擊按鈕,瀏覽器將通過 POST 請求將表單數據發送到服務端,運行默認 action。

[!NOTE] action 總是使用 POST 請求,因為 GET 請求不應該有副作用。

我們還可以通過添加 action 屬性,調用來自其他頁面的 action (例如,如果根佈局中的導航欄有一個登錄小部件):

/// file: src/routes/+layout.svelte
<form method="POST" action="/login">
    <!-- content -->
</form>

命名 actions

頁面可以根據需要擁有多個命名 action ,而不是隻有一個 default action:

/// file: src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
---    default: async (event) => {---
+++    login: async (event) => {+++
    // TODO log the user in
  },
+++    register: async (event) => {
    // TODO register the user
  }+++
};

要調用命名 action ,添加一個以 / 字符為前綴的查詢參數:

<!--- file: src/routes/login/+page.svelte --->
<form method="POST" action="?/register">
<!--- file: src/routes/+layout.svelte --->
<form method="POST" action="/login?/register">

除了 action 屬性,我們還可以在按鈕上使用 formaction 屬性,將相同的表單數據 POST 到與父 <form> 不同的 action :

/// file: src/routes/login/+page.svelte
<form method="POST" +++action="?/login"+++>
  <label>
    Email
    <input name="email" type="email">
  </label>
  <label>
    Password
    <input name="password" type="password">
  </label>
  <button>Log in</button>
  +++<button formaction="?/register">Register</button>+++
</form>
[!NOTE] 我們不能在命名 action 旁邊有默認 action ,因為如果您在沒有重定向的情況下 POST 到命名 action ,查詢參數會保留在 URL 中,這意味着下一個默認 POST 將通過之前的命名 action 進行處理。

action 的結構

每個 action 接收一個 RequestEvent 對象,允許您使用 request.formData() 讀取數據。在處理請求之後(例如,通過設置 cookie 讓用户登錄),action 可以響應數據,這些數據將在對應頁面的 form 屬性以及整個應用範圍的 page.form 中可用,直到下一次更新。

/// file: src/routes/login/+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/db';

// @filename: index.js
// ---cut---
import * as db from '$lib/server/db';

/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
  const user = await db.getUserFromSession(cookies.get('sessionid'));
  return { user };
}

/** @satisfies {import('./$types').Actions} */
export const actions = {
  login: async ({ cookies, request }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');

    const user = await db.getUser(email);
    cookies.set('sessionid', await db.createSession(user), { path: '/' });

    return { success: true };
  },
  register: async (event) => {
    // TODO register the user
  }
};
<!--- file: src/routes/login/+page.svelte --->
<script>
  /** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
  let { data, form } = $props();
</script>

{#if form?.success}
  <!-- 這個消息是短暫的;它存在是因為頁面是響應表單提交而渲染的。如果用户重新加載,消息將消失 -->
  <p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}
[!LEGACY]
在 Svelte 4 中,您將使用 export let dataexport let form 來聲明屬性

驗證錯誤

如果請求因數據無效而無法處理,您可以將驗證錯誤 —— 以及之前提交的表單值 —— 返回給用户,以便他們可以重試。fail 函數允許您返回一個 HTTP 狀態碼(通常是 400 或 422,用於驗證錯誤)以及數據。狀態碼可以通過 page.status 獲取,數據可以通過 form 獲取:

/// file: src/routes/login/+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/db';

// @filename: index.js
// ---cut---
+++import { fail } from '@sveltejs/kit';+++
import * as db from '$lib/server/db';

/** @satisfies {import('./$types').Actions} */
export const actions = {
  login: async ({ cookies, request }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');

+++        if (!email) {
      return fail(400, { email, missing: true });
    }+++

    const user = await db.getUser(email);

+++        if (!user || user.password !== db.hash(password)) {
      return fail(400, { email, incorrect: true });
    }+++

    cookies.set('sessionid', await db.createSession(user), { path: '/' });

    return { success: true };
  },
  register: async (event) => {
    // TODO register the user
  }
};
[!NOTE] 請注意,作為預防措施,我們只將電子郵件返回給頁面 —— 而不是密碼。
/// file: src/routes/login/+page.svelte
<form method="POST" action="?/login">
+++    {#if form?.missing}<p class="error">郵箱字段為必填項</p>{/if}
  {#if form?.incorrect}<p class="error">憑據無效!</p>{/if}+++
  <label>
    Email
    <input name="email" type="email" +++value={form?.email ?? ''}+++>
  </label>
  <label>
    Password
    <input name="password" type="password">
  </label>
  <button>Log in</button>
  <button formaction="?/register">Register</button>
</form>

返回的數據必須可序列化為 JSON。除此之外,結構完全由您決定。例如,如果頁面上有多個表單,您可以使用 id 屬性或類似的方式區分返回的 form 數據對應哪個 <form>

重定向

重定向(和錯誤)與 load 中的工作方式完全相同:

// @errors: 2345
/// file: src/routes/login/+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/db';

// @filename: index.js
// ---cut---
import { fail, +++redirect+++ } from '@sveltejs/kit';
import * as db from '$lib/server/db';

/** @satisfies {import('./$types').Actions} */
export const actions = {
  login: async ({ cookies, request, +++url+++ }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');

    const user = await db.getUser(email);
    if (!user) {
      return fail(400, { email, missing: true });
    }

    if (user.password !== db.hash(password)) {
      return fail(400, { email, incorrect: true });
    }

    cookies.set('sessionid', await db.createSession(user), { path: '/' });

+++        if (url.searchParams.has('redirectTo')) {
      redirect(303, url.searchParams.get('redirectTo'));
    }+++

    return { success: true };
  },
  register: async (event) => {
    // TODO register the user
  }
};

加載數據

action 運行後,頁面將重新渲染(除非發生重定向或意外錯誤), action 的返回值將作為 form 屬性提供給頁面。這意味着頁面的 load 函數將在 action 完成後運行。

請注意,handle 在 action 被調用之前運行,並且不會在 load 函數之前重新運行。這意味着,例如,如果您使用 handle 根據 cookie 填充 event.locals,則在 action 中設置或刪除 cookie 時,必須更新 event.locals

/// file: src/hooks.server.js
// @filename: ambient.d.ts
declare namespace App {
  interface Locals {
    user: {
      name: string;
    } | null
  }
}

// @filename: global.d.ts
declare global {
  function getUser(sessionid: string | undefined): {
    name: string;
  };
}

export {};

// @filename: index.js
// ---cut---
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
  event.locals.user = await getUser(event.cookies.get('sessionid'));
  return resolve(event);
}
/// file: src/routes/account/+page.server.js
// @filename: ambient.d.ts
declare namespace App {
  interface Locals {
    user: {
      name: string;
    } | null
  }
}

// @filename: index.js
// ---cut---
/** @type {import('./$types').PageServerLoad} */
export function load(event) {
  return {
    user: event.locals.user
  };
}

/** @satisfies {import('./$types').Actions} */
export const actions = {
  logout: async (event) => {
    event.cookies.delete('sessionid', { path: '/' });
    event.locals.user = null;
  }
};

漸進式增強

在前面的章節中,我們構建了一個在沒有客户端 JavaScript 的情況下工作的 /login action —— 沒有 fetch。這很好,但當 JavaScript 可用 時,我們可以漸進式增強表單交互,以提供更好的用户體驗。

use:enhance

漸進式增強表單的最簡單方法是添加 use:enhance action :

/// file: src/routes/login/+page.svelte
<script>
  +++import { enhance } from '$app/forms';+++

  /** @type {{ form: import('./$types').ActionData }} */
  let { form } = $props();
</script>

<form method="POST" +++use:enhance+++>

[!NOTE] use:enhance 只能與 method="POST" 的表單一起使用。它將無法與 method="GET" 一起工作,後者是未指定方法的表單的默認方法。在未指定 method="POST" 的表單上嘗試使用 use:enhance 將導致錯誤。

[!NOTE] 是的,enhance action 和 <form action> 都叫做 'action',這些文檔充滿了各種 action。抱歉。

沒有參數時,use:enhance 將模擬瀏覽器原生行為,只是不進行完整頁面重載。它將:

  • 在成功或無效響應時更新 form 屬性、page.formpage.status,但僅當 action 在您提交的同一頁面上時。例如,如果您的表單看起來像 <form action="/somewhere/else" ..>form 屬性和 page.form 狀態將 不會 更新。這是因為在本地表單提交的情況下,您將被重定向到 action 所在的頁面。如果您希望無論如何都能更新,使用 applyAction
  • 重置 <form> 元素
  • 在成功響應時使用 invalidateAll 使所有數據失效
  • 在重定向響應時調用 goto
  • 如果發生錯誤,渲染最近的 +error 邊界
  • 將焦點重置到適當的元素

自定義 use:enhance

要自定義行為,您可以提供一個 SubmitFunction,它會在表單提交前立即運行,並(可選地)返回一個隨 ActionResult 一起運行的回調。請注意,如果您返回一個回調,上述默認行為將不會被觸發。要恢復默認行為,請調用 update

<form
  method="POST"
  use:enhance={({ formElement, formData, action, cancel, submitter }) => {
    // `formElement` 是這個 `<form>` 元素
    // `formData` 是即將提交的 `FormData` 對象
    // `action` 是表單提交的 URL
    // 調用 `cancel()` 將阻止提交
    // `submitter` 是導致表單提交的 `HTMLElement`

    return async ({ result, update }) => {
      // `result` 是一個 `ActionResult` 對象
      // `update` 是一個觸發默認邏輯的函數,如果沒有設置此回調
    };
  }}
>

您可以使用這些函數來顯示和隱藏加載界面等。

如果您返回一個回調,您可能需要重現部分默認的 use:enhance 行為,但在成功響應時不使所有數據失效。您可以使用 applyAction 來實現:

/// file: src/routes/login/+page.svelte
<script>
  import { enhance, +++applyAction+++ } from '$app/forms';

  /** @type {{ form: import('./$types').ActionData }} */
  let { form } = $props();
</script>

<form
  method="POST"
  use:enhance={({ formElement, formData, action, cancel }) => {
    return async ({ result }) => {
      // `result` 是一個 `ActionResult` 對象
+++            if (result.type === 'redirect') {
        goto(result.location);
      } else {
        await applyAction(result);
      }+++
    };
  }}
>

applyAction(result) 的行為取決於 result.type

  • success, failure — 將 page.status 設置為 result.status,並將 formpage.form 更新為 result.data(無論您從哪裏提交,這與 enhanceupdate 形成對比)
  • redirect — 調用 goto(result.location, { invalidateAll: true })
  • error — 使用 result.error 渲染最近的 +error 邊界

在所有情況下,焦點將被重置。

自定義事件監聽器

我們也可以不使用 use:enhance,在 <form> 上使用普通的事件監聽器,自己實現漸進式增強:

<!--- file: src/routes/login/+page.svelte --->
<script>
  import { invalidateAll, goto } from '$app/navigation';
  import { applyAction, deserialize } from '$app/forms';

  /** @type {{ form: import('./$types').ActionData }} */
  let { form } = $props();

  /** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}} event */
  async function handleSubmit(event) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);

    const response = await fetch(event.currentTarget.action, {
      method: 'POST',
      body: data
    });

    /** @type {import('@sveltejs/kit').ActionResult} */
    const result = deserialize(await response.text());

    if (result.type === 'success') {
      // 重新運行所有 `load` 函數,跟隨成功的更新
      await invalidateAll();
    }

    applyAction(result);
  }
</script>

<form method="POST" onsubmit={handleSubmit}>
  <!-- content -->
</form>

請注意,在使用 $app/forms 中相應的方法進一步處理響應之前,需要 deserialize 響應。僅 JSON.parse() 是不夠的,因為表單 action(如 load 函數)也支持返回 DateBigInt 對象。

如果您在 +page.server.js 旁邊有一個 +server.jsfetch 請求將默認路由到那裏。要改為 POST+page.server.js 中的 action ,請使用自定義的 x-sveltekit-action 頭:

const response = await fetch(this.action, {
  method: 'POST',
  body: data,
+++    headers: {
    'x-sveltekit-action': 'true'
  }+++
});

替代方案

表單 action 是向服務端發送數據的首選方法,因為它們可以漸進式增強,但您也可以使用 +server.js 文件來公開(例如)一個 JSON API。以下是這種交互的示例:

<!--- file: src/routes/send-message/+page.svelte --->
<script>
  function rerun() {
    fetch('/api/ci', {
      method: 'POST'
    });
  }
</script>

<button onclick={rerun}>Rerun CI</button>
// @errors: 2355 1360 2322
/// file: src/routes/api/ci/+server.js
/** @type {import('./$types').RequestHandler} */
export function POST() {
    // do something
}

GET 與 POST

如我們所見,要調用表單 action ,必須使用 method="POST"

有些表單不需要向服務端 POST 數據 —— 例如搜索輸入。對於這些表單,您可以使用 method="GET"(或等效地,不指定 method),SvelteKit 將像處理 <a> 元素一樣處理它們,使用客户端路由而不是完整頁面導航:

<form action="/search">
    <label>
        Search
        <input name="q" />
    </label>
</form>

提交此表單將導航到 /search?q=... 並調用您的 load 函數,但不會調用 action 。與 <a> 元素一樣,您可以在 <form> 上設置 data-sveltekit-reloaddata-sveltekit-replacestatedata-sveltekit-keepfocus 以及 data-sveltekit-noscroll 屬性,以控制路由器的行為。

進一步閲讀

  • 教程:表單

Svelte 中文文檔

點擊查看中文文檔 - SvelteKit 表單 actions。

系統學習 Svelte,歡迎入手小冊《Svelte 開發指南》。語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

此外我還寫過 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答讀者問等 14 個系列文章, 全系列文章目錄:https://github.com/mqyqingfeng/Blog

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

Add a new 评论

Some HTML is okay.