跳至主要内容

核心概念

表单操作

在 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 请求将默认路由到那里。要改为向 +page.server.js 中的操作 POST,请使用自定义 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 属性来控制路由器的行为。

延伸阅读

上一个 加载数据
下一个 页面选项