前言
Svelte,一個語法簡潔、入門容易,面向未來的前端框架。
從 Svelte 誕生之初,就備受開發者的喜愛,根據統計,從 2019 年到 2024 年,連續 6 年一直是開發者最感興趣的前端框架 No.1:
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 data和export 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] 是的,
enhanceaction 和<form action>都叫做 'action',這些文檔充滿了各種 action。抱歉。
沒有參數時,use:enhance 將模擬瀏覽器原生行為,只是不進行完整頁面重載。它將:
- 在成功或無效響應時更新
form屬性、page.form和page.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,並將form和page.form更新為result.data(無論您從哪裏提交,這與enhance的update形成對比)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 函數)也支持返回 Date 或 BigInt 對象。
如果您在 +page.server.js 旁邊有一個 +server.js,fetch 請求將默認路由到那裏。要改為 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-reload、data-sveltekit-replacestate、data-sveltekit-keepfocus 以及 data-sveltekit-noscroll 屬性,以控制路由器的行為。
進一步閲讀
- 教程:表單
Svelte 中文文檔
點擊查看中文文檔 - SvelteKit 表單 actions。
系統學習 Svelte,歡迎入手小冊《Svelte 開發指南》。語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!
此外我還寫過 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答讀者問等 14 個系列文章, 全系列文章目錄:https://github.com/mqyqingfeng/Blog
歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。