核心概念
表单操作
在 GitHub 上编辑此页面+page.server.js
文件可以导出操作,使用 <form>
元素允许你将数据 POST
到服务器。
使用 <form>
时,客户端 JavaScript 是可选的,但你可以轻松地使用 JavaScript 逐步增强你的表单交互,以提供最佳用户体验。
默认操作永久链接
在最简单的情况下,页面声明一个 default
操作
ts
/** @type {import('./$types').Actions} */export constactions = {default : async (event ) => {// TODO log the user in}};
ts
import type {Actions } from './$types';export constactions = {default : async (event ) => {// TODO log the user in},} satisfiesActions ;
要从 /login
页面调用此操作,只需添加一个 <form>
— 无需 JavaScript
<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
属性(指向页面)从其他页面调用操作(例如,如果根布局导航中有一个登录小部件)
<form method="POST" action="/login">
<!-- content -->
</form>
命名操作永久链接
页面可以拥有任意数量的命名操作,而不是一个 default
操作
/** @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
}
};
要调用命名操作,请添加一个查询参数,其名称前缀为 /
字符
<form method="POST" action="?/register">
<form method="POST" action="/login?/register">
除了 action
属性,我们还可以在按钮上使用 formaction
属性,将相同表单数据 POST
到与父 <form>
不同的操作
<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
获得,直到下一次更新。
ts
/** @type {import('./$types').PageServerLoad} */export async functionCannot find name 'db'.2304Cannot find name 'db'.load ({cookies }) {constCannot find name 'db'.2304Cannot find name 'db'.user = awaitdb .getUserFromSession ( cookies .get ('sessionid'));return {user };}/** @type {import('./$types').Actions} */export constactions = {login : async ({cookies ,request }) => {constdata = awaitrequest .formData ();constdata .get ('email');constpassword =data .get ('password');constuser = awaitdb .getUser (cookies .set ('sessionid', awaitdb .createSession (user ), {path : '/' });return {success : true };},register : async (event ) => {// TODO register the user}};
ts
import type {PageServerLoad ,Actions } from './$types';Cannot find name 'db'.2304Cannot find name 'db'.export constCannot find name 'db'.2304Cannot find name 'db'.load :PageServerLoad = async ({cookies }) => {constuser = awaitdb .getUserFromSession (cookies .get ('sessionid'));return {user };};export constactions = {login : async ({cookies ,request }) => {constdata = awaitrequest .formData ();constdata .get ('email');constpassword =data .get ('password');constuser = awaitdb .getUser (cookies .set ('sessionid', awaitdb .createSession (user ), {path : '/' });return {success : true };},register : async (event ) => {// TODO register the user},} satisfiesActions ;
<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}
<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
获得。
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
}
};
请注意,作为预防措施,我们只将电子邮件返回给页面,而不是密码。
<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
中完全相同。
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
。
ts
/** @type {import('@sveltejs/kit').Handle} */export async functionhandle ({event ,resolve }) {event .locals .user = awaitgetUser (event .cookies .get ('sessionid'));returnresolve (event );}
ts
import type {Handle } from '@sveltejs/kit';export consthandle :Handle = async ({event ,resolve }) => {event .locals .user = awaitgetUser (event .cookies .get ('sessionid'));returnresolve (event );};
ts
/** @type {import('./$types').PageServerLoad} */export functionload (event ) {return {user :event .locals .user };}/** @type {import('./$types').Actions} */export constactions = {logout : async (event ) => {event .cookies .delete ('sessionid', {path : '/' });event .locals .user = null;}};
ts
import type {PageServerLoad ,Actions } from './$types';export constload :PageServerLoad = (event ) => {return {user :event .locals .user ,};};export constactions = {logout : async (event ) => {event .cookies .delete ('sessionid', {path : '/' });event .locals .user = null;},} satisfiesActions ;
渐进增强永久链接
在前面的部分中,我们构建了一个 /login
操作,它无需客户端 JavaScript 即可工作——看不到 fetch
。这很好,但是当 JavaScript 可用时,我们可以逐步增强表单交互,以提供更好的用户体验。
use:enhance永久链接
逐步增强表单的最简单方法是添加 use:enhance
操作
<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
来做到这一点
<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
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>
上使用一个普通的事件侦听器
<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>
<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
中的相应方法进一步处理响应之前,需要对其进行 deserialize
。JSON.parse()
不够,因为表单操作(如 load
函数)还支持返回 Date
或 BigInt
对象。
如果您在 +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。以下是这种交互的外观
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button on:click={rerun}>Rerun CI</button>
<script lang="ts">
function rerun() {
fetch('/api/ci', {
method: 'POST',
});
}
</script>
<button on:click={rerun}>Rerun CI</button>
ts
/** @type {import('./$types').RequestHandler} */export functionPOST () {// do something}
ts
import type {RequestHandler } from './$types';export constPOST :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-reload
、data-sveltekit-replacestate
、data-sveltekit-keepfocus
和 data-sveltekit-noscroll
属性来控制路由器的行为。