前言
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:
js
/// 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:
svelte
<!--- 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 (例如,如果根布局中的导航栏有一个登录小部件):
html
/// file: src/routes/+layout.svelte
<form method="POST" action="/login">
<!-- content -->
</form>
命名 actions
页面可以根据需要拥有多个命名 action ,而不是只有一个 default
action:
js
/// 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 ,添加一个以 /
字符为前缀的查询参数:
svelte
<!--- file: src/routes/login/+page.svelte --->
<form method="POST" action="?/register">
svelte
<!--- file: src/routes/+layout.svelte --->
<form method="POST" action="/login?/register">
除了 action
属性,我们还可以在按钮上使用 formaction
属性,将相同的表单数据 POST
到与父 <form>
不同的 action :
svelte
/// 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
中可用,直到下一次更新。
js
/// 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
}
};
svelte
<!--- 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
获取:
js
/// 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\] 请注意,作为预防措施,我们只将电子邮件返回给页面 ------ 而不是密码。
svelte
/// 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
中的工作方式完全相同:
js
// @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
:
js
/// 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);
}
js
/// 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 :
svelte
/// 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 和 `
没有参数时,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
。
svelte
<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
来实现:
svelte
/// 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>
上使用普通的事件监听器,自己实现渐进式增强:
svelte
<!--- 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
头:
js
const response = await fetch(this.action, {
method: 'POST',
body: data,
+++ headers: {
'x-sveltekit-action': 'true'
}+++
});
替代方案
表单 action 是向服务端发送数据的首选方法,因为它们可以渐进式增强,但您也可以使用 +server.js
文件来公开(例如)一个 JSON API。以下是这种交互的示例:
svelte
<!--- file: src/routes/send-message/+page.svelte --->
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button onclick={rerun}>Rerun CI</button>
js
// @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>
元素一样处理它们,使用客户端路由而不是完整页面导航:
html
<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 个系列文章, 全系列文章目录:github.com/mqyqingfeng...
欢迎围观我的"网页版朋友圈"、加入"冴羽·成长陪伴社群",踏上"前端大佬成长之路"。