跳至主要內容

核心概念

表單動作

在 GitHub 上編輯此頁面

+page.server.js 檔案可以匯出動作,讓您可以使用 <form> 元素將資料 POST 到伺服器。

使用 <form> 時,客戶端 JavaScript 是選用的,但您可以輕鬆地使用 JavaScript 逐步增強您的表單互動,以提供最佳使用者體驗。

預設動作

在最簡單的情況下,頁面會宣告一個 default 動作

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

若要從 /login 頁面呼叫此動作,只要加入一個 <form>,不需要 JavaScript

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 要求將表單資料傳送至伺服器,執行預設動作。

動作總是使用 POST 要求,因為 GET 要求不應產生副作用。

我們也可以從其他頁面呼叫動作(例如,如果根佈局的導覽列中有登入小工具),方法是加入 action 屬性,指向該頁面

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

命名動作

頁面可以有任意數量的命名動作,而不只一個 default 動作

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

若要呼叫命名動作,請加入一個查詢參數,其名稱前面加上 / 字元

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

除了 action 屬性之外,我們可以使用按鈕上的 formaction 屬性,將相同的表單資料 POST 到與父 <form> 不同的動作。

src/routes/login/+page.svelte
<form method="POST">
<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>

我們無法在命名動作旁邊有預設動作,因為如果你在沒有重新導向的情況下 POST 到命名動作,查詢參數會保留在 URL 中,這表示下一個預設 POST 會從之前的命名動作開始。

動作的解剖

每個動作都會收到一個 RequestEvent 物件,讓你能夠使用 request.formData() 讀取資料。在處理請求之後(例如透過設定 cookie 讓使用者登入),動作可以使用資料回應,這些資料會透過對應頁面上的 form 屬性,以及在下次更新之前透過 $page.form 在整個應用程式中提供。

src/routes/login/+page.server.js
ts
/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
Cannot find name 'db'.2304Cannot find name 'db'.
const user = await db.getUserFromSession(cookies.get('sessionid'));
Cannot find name 'db'.2304Cannot find name 'db'.
return { user };
}
/** @type {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
}
};
src/routes/login/+page.server.ts
ts
import type { PageServerLoad, Actions } from './$types';
Cannot find name 'db'.2304Cannot find name 'db'.
export const load: PageServerLoad = async ({ cookies }) => {
Cannot find name 'db'.2304Cannot find name 'db'.
const user = await db.getUserFromSession(cookies.get('sessionid'));
return { user };
};
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
},
} satisfies Actions;
src/routes/login/+page.svelte
<script>
	/** @type {import('./$types').PageData} */
	export let data;

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

{#if form?.success}
	<!-- this message is ephemeral; it exists because the page was rendered in
		   response to a form submission. it will vanish if the user reloads -->
	<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}
src/routes/login/+page.svelte
<script lang="ts">
	import type { PageData, ActionData } from './$types';
	
	export let data: PageData;
	
	export let form: ActionData;
</script>

{#if form?.success}
	<!-- this message is ephemeral; it exists because the page was rendered in
		   response to a form submission. it will vanish if the user reloads -->
	<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}

驗證錯誤

如果請求無法因為資料無效而處理,你可以將驗證錯誤(以及先前提交的表單值)傳回給使用者,以便他們可以重試。fail 函數讓你傳回 HTTP 狀態碼(通常在驗證錯誤的情況下為 400 或 422),以及資料。狀態碼可透過 $page.status 取得,而資料可透過 form 取得。

src/routes/login/+page.server.js
import { fail } from '@sveltejs/kit';

/** @type {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 !== 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
	}
};

請注意,作為預防措施,我們只會將電子郵件傳回頁面,而不是密碼。

src/routes/login/+page.svelte
<form method="POST" action="?/login">
	{#if form?.missing}<p class="error">The email field is required</p>{/if}
	{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
	<label>
		Email
		<input name="email" type="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 中完全相同。

src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';

/** @type {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 !== 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
	}
};

載入資料

在動作執行之後,頁面將會重新呈現(除非發生重新導向或意外錯誤),動作的回傳值會作為 form 道具提供給頁面。這表示你的頁面 load 函數會在動作完成後執行。

請注意,handle 會在動作呼叫之前執行,且不會在 load 函式之前再次執行。這表示,例如,如果您使用 handle 根據 cookie 填入 event.locals,您必須在動作中設定或刪除 cookie 時更新 event.locals

src/hooks.server.js
ts
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
}
src/hooks.server.ts
ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
};
src/routes/account/+page.server.js
ts
/** @type {import('./$types').PageServerLoad} */
export function load(event) {
return {
user: event.locals.user
};
}
/** @type {import('./$types').Actions} */
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid', { path: '/' });
event.locals.user = null;
}
};
src/routes/account/+page.server.ts
ts
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = (event) => {
return {
user: event.locals.user,
};
};
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid', { path: '/' });
event.locals.user = null;
},
} satisfies Actions;

漸進式增強

在前面的區段中,我們建立了一個 /login 動作,它可以在沒有用戶端 JavaScript 的情況下運作,完全不需要 fetch。這很棒,但當 JavaScript 可用 時,我們可以漸進式增強我們的表單互動,以提供更好的使用者體驗。

use:enhance

漸進式增強表單最簡單的方法是新增 use:enhance 動作

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

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

<form method="POST" use:enhance>

沒錯,enhance 動作和 <form action> 都稱為「動作」,這有點令人困惑。這些文件充滿了動作。抱歉。

use:enhance 在沒有參數的情況下,將模擬瀏覽器的原生行為,只是不會重新載入整頁。它會

  • 在成功或無效的回應中更新 form 屬性、$page.form$page.status,但前提是動作與您提交的頁面在同一個頁面上。例如,如果您的表單看起來像 <form action="/somewhere/else" ..>,則 form$page 不會 更新。這是因為在原生表單提交案例中,您會被重新導向到動作所在的頁面。如果您希望無論如何都更新它們,請使用 applyAction
  • 重設 <form> 元素
  • 在成功的回應中使用 invalidateAll 使所有資料失效
  • 在重新導向回應中呼叫 goto
  • 如果發生錯誤,則呈現最近的 +error 邊界
  • 將焦點重設 到適當的元素

自訂 use:enhance

若要自訂行為,你可以提供一個在表單提交前立即執行的 SubmitFunction,並(選擇性地)傳回一個與 ActionResult 一起執行的回呼。請注意,如果你傳回一個回呼,則上述提到的預設行為將不會觸發。若要取回該行為,請呼叫 update

<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel, submitter }) => {
		// `formElement` is this `<form>` element
		// `formData` is its `FormData` object that's about to be submitted
		// `action` is the URL to which the form is posted
		// calling `cancel()` will prevent the submission
		// `submitter` is the `HTMLElement` that caused the form to be submitted

		return async ({ result, update }) => {
			// `result` is an `ActionResult` object
			// `update` is a function which triggers the default logic that would be triggered if this callback wasn't set
		};
	}}
>

你可以使用這些函數來顯示和隱藏載入 UI 等。

如果你傳回一個回呼,你可能需要重現預設 use:enhance 行為的一部分,但不必在成功回應時使所有資料失效。你可以使用 applyAction 執行此操作。

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

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

<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel }) => {

		return async ({ result }) => {
			// `result` is an `ActionResult` object
			if (result.type === 'redirect') {
				goto(result.location);
			} else {
				await applyAction(result);
			}
		};
	}}
>

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

  • successfailure — 將 $page.status 設定為 result.status,並將 form$page.form 更新為 result.data(無論你從何處提交,這與 enhance 中的 update 相反)
  • redirect — 呼叫 goto(result.location, { invalidateAll: true })
  • error — 使用 result.error 呈現最近的 +error 邊界

在所有情況下,焦點將會重設

自訂事件監聽器

我們也可以在沒有 use:enhance 的情況下,使用 <form> 上的正常事件監聽器,自行實作漸進式增強。

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

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

	/** @type {any} */
	let error;

	/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
	async function handleSubmit(event) {
		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') {
			// rerun all `load` functions, following the successful update
			await invalidateAll();
		}

		applyAction(result);
	}
</script>

<form method="POST" on:submit|preventDefault={handleSubmit}>
	<!-- content -->
</form>
src/routes/login/+page.svelte
<script lang="ts">
	import { invalidateAll, goto } from '$app/navigation';
	import { applyAction, deserialize } from '$app/forms';
	
	import type { ActionData } from './$types';
	import type { ActionResult } from '@sveltejs/kit';
	
	export let form: ActionData;
	
	let error: any;
	
	async function handleSubmit(event: { currentTarget: EventTarget & HTMLFormElement }) {
		const data = new FormData(event.currentTarget);
	
		const response = await fetch(event.currentTarget.action, {
			method: 'POST',
			body: data,
		});
	
		const result: ActionResult = deserialize(await response.text());
	
		if (result.type === 'success') {
			// rerun all `load` functions, following the successful update
			await invalidateAll();
		}
	
		applyAction(result);
	}
</script>

<form method="POST" on:submit|preventDefault={handleSubmit}>
	<!-- content -->
</form>

請注意,在使用 $app/forms 中的對應方法進一步處理回應之前,你需要對回應進行 deserializeJSON.parse() 不夠用,因為表單動作(如 load 函數)也支援傳回 DateBigInt 物件。

如果你在 +page.server.js 旁有一個 +server.js,則 fetch 要求會預設路由到那裡。若要改為 POST+page.server.js 中的動作,請使用自訂 x-sveltekit-action 標頭

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

替代方案

表單動作是將資料傳送至伺服器的方式,因為它們可以漸進式增強,但你也可以使用 +server.js 檔案來公開(例如)JSON API。以下是如何執行此類互動

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

<button on:click={rerun}>Rerun CI</button>
send-message/+page.svelte
<script lang="ts">
	function rerun() {
		fetch('/api/ci', {
			method: 'POST',
		});
	}
</script>

<button on:click={rerun}>Rerun CI</button>
api/ci/+server.js
ts
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}
api/ci/+server.ts
ts
import type { RequestHandler } from './$types';
export const POST: RequestHandler = () => {
// do something
};

GET 與 POST

如我們所見,若要呼叫表單動作,你必須使用 method="POST"

有些表單不需要 POST 資料到伺服器,例如搜尋輸入。對於這些表單,你可以使用 method="GET"(或等效地,完全不使用 method),而 SvelteKit 會將它們視為 <a> 元素,使用用戶端路由器,而不是完整的頁面導覽

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

提交此表單將導航至 /search?q=... 並呼叫您的載入函式,但不會呼叫動作。與 <a> 元素一樣,您可以設定 <form> 上的 data-sveltekit-reloaddata-sveltekit-replacestatedata-sveltekit-keepfocusdata-sveltekit-noscroll 屬性,以控制路由器的行為。

進一步閱讀

前一頁 載入資料
下一頁 頁面選項