前言
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:
1/// file: src/routes/login/+page.server.js 2/** @satisfies {import('./$types').Actions} */ 3export const actions = { 4 default: async (event) => { 5 // TODO log the user in 6 } 7};
要从 /login 页面调用此 action,只需添加一个 <form> —— 不需要 JavaScript:
1<!--- file: src/routes/login/+page.svelte ---> 2<form method="POST"> 3 <label> 4 Email 5 <input name="email" type="email"> 6 </label> 7 <label> 8 Password 9 <input name="password" type="password"> 10 </label> 11 <button>Log in</button> 12</form>
如果有人点击按钮,浏览器将通过 POST 请求将表单数据发送到服务端,运行默认 action。
[!NOTE] action 总是使用
POST请求,因为GET请求不应该有副作用。
我们还可以通过添加 action 属性,调用来自其他页面的 action (例如,如果根布局中的导航栏有一个登录小部件):
1/// file: src/routes/+layout.svelte 2<form method="POST" action="/login"> 3 <!-- content --> 4</form>
命名 actions
页面可以根据需要拥有多个命名 action ,而不是只有一个 default action:
1/// file: src/routes/login/+page.server.js 2/** @satisfies {import('./$types').Actions} */ 3export const actions = { 4--- default: async (event) => {--- 5+++ login: async (event) => {+++ 6 // TODO log the user in 7 }, 8+++ register: async (event) => { 9 // TODO register the user 10 }+++ 11};
要调用命名 action ,添加一个以 / 字符为前缀的查询参数:
1<!--- file: src/routes/login/+page.svelte ---> 2<form method="POST" action="?/register">
1<!--- file: src/routes/+layout.svelte ---> 2<form method="POST" action="/login?/register">
除了 action 属性,我们还可以在按钮上使用 formaction 属性,将相同的表单数据 POST 到与父 <form> 不同的 action :
1/// file: src/routes/login/+page.svelte 2<form method="POST" +++action="?/login"+++> 3 <label> 4 Email 5 <input name="email" type="email"> 6 </label> 7 <label> 8 Password 9 <input name="password" type="password"> 10 </label> 11 <button>Log in</button> 12 +++<button formaction="?/register">Register</button>+++ 13</form>
[!NOTE] 我们不能在命名 action 旁边有默认 action ,因为如果您在没有重定向的情况下
POST到命名 action ,查询参数会保留在 URL 中,这意味着下一个默认POST将通过之前的命名 action 进行处理。
action 的结构
每个 action 接收一个 RequestEvent 对象,允许您使用 request.formData() 读取数据。在处理请求之后(例如,通过设置 cookie 让用户登录),action 可以响应数据,这些数据将在对应页面的 form 属性以及整个应用范围的 page.form 中可用,直到下一次更新。
1/// file: src/routes/login/+page.server.js 2// @filename: ambient.d.ts 3declare module '$lib/server/db'; 4 5// @filename: index.js 6// ---cut--- 7import * as db from '$lib/server/db'; 8 9/** @type {import('./$types').PageServerLoad} */ 10export async function load({ cookies }) { 11 const user = await db.getUserFromSession(cookies.get('sessionid')); 12 return { user }; 13} 14 15/** @satisfies {import('./$types').Actions} */ 16export const actions = { 17 login: async ({ cookies, request }) => { 18 const data = await request.formData(); 19 const email = data.get('email'); 20 const password = data.get('password'); 21 22 const user = await db.getUser(email); 23 cookies.set('sessionid', await db.createSession(user), { path: '/' }); 24 25 return { success: true }; 26 }, 27 register: async (event) => { 28 // TODO register the user 29 } 30};
1<!--- file: src/routes/login/+page.svelte ---> 2<script> 3 /** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */ 4 let { data, form } = $props(); 5</script> 6 7{#if form?.success} 8 <!-- 这个消息是短暂的;它存在是因为页面是响应表单提交而渲染的。如果用户重新加载,消息将消失 --> 9 <p>Successfully logged in! Welcome back, {data.user.name}</p> 10{/if}
[!LEGACY] 在 Svelte 4 中,您将使用
export let data和export let form来声明属性
验证错误
如果请求因数据无效而无法处理,您可以将验证错误 —— 以及之前提交的表单值 —— 返回给用户,以便他们可以重试。fail 函数允许您返回一个 HTTP 状态码(通常是 400 或 422,用于验证错误)以及数据。状态码可以通过 page.status 获取,数据可以通过 form 获取:
1/// file: src/routes/login/+page.server.js 2// @filename: ambient.d.ts 3declare module '$lib/server/db'; 4 5// @filename: index.js 6// ---cut--- 7+++import { fail } from '@sveltejs/kit';+++ 8import * as db from '$lib/server/db'; 9 10/** @satisfies {import('./$types').Actions} */ 11export const actions = { 12 login: async ({ cookies, request }) => { 13 const data = await request.formData(); 14 const email = data.get('email'); 15 const password = data.get('password'); 16 17+++ if (!email) { 18 return fail(400, { email, missing: true }); 19 }+++ 20 21 const user = await db.getUser(email); 22 23+++ if (!user || user.password !== db.hash(password)) { 24 return fail(400, { email, incorrect: true }); 25 }+++ 26 27 cookies.set('sessionid', await db.createSession(user), { path: '/' }); 28 29 return { success: true }; 30 }, 31 register: async (event) => { 32 // TODO register the user 33 } 34};
[!NOTE] 请注意,作为预防措施,我们只将电子邮件返回给页面 —— 而不是密码。
1/// file: src/routes/login/+page.svelte 2<form method="POST" action="?/login"> 3+++ {#if form?.missing}<p class="error">邮箱字段为必填项</p>{/if} 4 {#if form?.incorrect}<p class="error">凭据无效!</p>{/if}+++ 5 <label> 6 Email 7 <input name="email" type="email" +++value={form?.email ?? ''}+++> 8 </label> 9 <label> 10 Password 11 <input name="password" type="password"> 12 </label> 13 <button>Log in</button> 14 <button formaction="?/register">Register</button> 15</form>
返回的数据必须可序列化为 JSON。除此之外,结构完全由您决定。例如,如果页面上有多个表单,您可以使用 id 属性或类似的方式区分返回的 form 数据对应哪个 <form>。
重定向
重定向(和错误)与 load 中的工作方式完全相同:
1// @errors: 2345 2/// file: src/routes/login/+page.server.js 3// @filename: ambient.d.ts 4declare module '$lib/server/db'; 5 6// @filename: index.js 7// ---cut--- 8import { fail, +++redirect+++ } from '@sveltejs/kit'; 9import * as db from '$lib/server/db'; 10 11/** @satisfies {import('./$types').Actions} */ 12export const actions = { 13 login: async ({ cookies, request, +++url+++ }) => { 14 const data = await request.formData(); 15 const email = data.get('email'); 16 const password = data.get('password'); 17 18 const user = await db.getUser(email); 19 if (!user) { 20 return fail(400, { email, missing: true }); 21 } 22 23 if (user.password !== db.hash(password)) { 24 return fail(400, { email, incorrect: true }); 25 } 26 27 cookies.set('sessionid', await db.createSession(user), { path: '/' }); 28 29+++ if (url.searchParams.has('redirectTo')) { 30 redirect(303, url.searchParams.get('redirectTo')); 31 }+++ 32 33 return { success: true }; 34 }, 35 register: async (event) => { 36 // TODO register the user 37 } 38};
加载数据
action 运行后,页面将重新渲染(除非发生重定向或意外错误), action 的返回值将作为 form 属性提供给页面。这意味着页面的 load 函数将在 action 完成后运行。
请注意,handle 在 action 被调用之前运行,并且不会在 load 函数之前重新运行。这意味着,例如,如果您使用 handle 根据 cookie 填充 event.locals,则在 action 中设置或删除 cookie 时,必须更新 event.locals:
1/// file: src/hooks.server.js 2// @filename: ambient.d.ts 3declare namespace App { 4 interface Locals { 5 user: { 6 name: string; 7 } | null 8 } 9} 10 11// @filename: global.d.ts 12declare global { 13 function getUser(sessionid: string | undefined): { 14 name: string; 15 }; 16} 17 18export {}; 19 20// @filename: index.js 21// ---cut--- 22/** @type {import('@sveltejs/kit').Handle} */ 23export async function handle({ event, resolve }) { 24 event.locals.user = await getUser(event.cookies.get('sessionid')); 25 return resolve(event); 26}
1/// file: src/routes/account/+page.server.js 2// @filename: ambient.d.ts 3declare namespace App { 4 interface Locals { 5 user: { 6 name: string; 7 } | null 8 } 9} 10 11// @filename: index.js 12// ---cut--- 13/** @type {import('./$types').PageServerLoad} */ 14export function load(event) { 15 return { 16 user: event.locals.user 17 }; 18} 19 20/** @satisfies {import('./$types').Actions} */ 21export const actions = { 22 logout: async (event) => { 23 event.cookies.delete('sessionid', { path: '/' }); 24 event.locals.user = null; 25 } 26};
渐进式增强
在前面的章节中,我们构建了一个在没有客户端 JavaScript 的情况下工作的 /login action —— 没有 fetch。这很好,但当 JavaScript 可用 时,我们可以渐进式增强表单交互,以提供更好的用户体验。
use:enhance
渐进式增强表单的最简单方法是添加 use:enhance action :
1/// file: src/routes/login/+page.svelte 2<script> 3 +++import { enhance } from '$app/forms';+++ 4 5 /** @type {{ form: import('./$types').ActionData }} */ 6 let { form } = $props(); 7</script> 8 9<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。
1<form 2 method="POST" 3 use:enhance={({ formElement, formData, action, cancel, submitter }) => { 4 // `formElement` 是这个 `<form>` 元素 5 // `formData` 是即将提交的 `FormData` 对象 6 // `action` 是表单提交的 URL 7 // 调用 `cancel()` 将阻止提交 8 // `submitter` 是导致表单提交的 `HTMLElement` 9 10 return async ({ result, update }) => { 11 // `result` 是一个 `ActionResult` 对象 12 // `update` 是一个触发默认逻辑的函数,如果没有设置此回调 13 }; 14 }} 15>
您可以使用这些函数来显示和隐藏加载界面等。
如果您返回一个回调,您可能需要重现部分默认的 use:enhance 行为,但在成功响应时不使所有数据失效。您可以使用 applyAction 来实现:
1/// file: src/routes/login/+page.svelte 2<script> 3 import { enhance, +++applyAction+++ } from '$app/forms'; 4 5 /** @type {{ form: import('./$types').ActionData }} */ 6 let { form } = $props(); 7</script> 8 9<form 10 method="POST" 11 use:enhance={({ formElement, formData, action, cancel }) => { 12 return async ({ result }) => { 13 // `result` 是一个 `ActionResult` 对象 14+++ if (result.type === 'redirect') { 15 goto(result.location); 16 } else { 17 await applyAction(result); 18 }+++ 19 }; 20 }} 21>
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> 上使用普通的事件监听器,自己实现渐进式增强:
1<!--- file: src/routes/login/+page.svelte ---> 2<script> 3 import { invalidateAll, goto } from '$app/navigation'; 4 import { applyAction, deserialize } from '$app/forms'; 5 6 /** @type {{ form: import('./$types').ActionData }} */ 7 let { form } = $props(); 8 9 /** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}} event */ 10 async function handleSubmit(event) { 11 event.preventDefault(); 12 const data = new FormData(event.currentTarget); 13 14 const response = await fetch(event.currentTarget.action, { 15 method: 'POST', 16 body: data 17 }); 18 19 /** @type {import('@sveltejs/kit').ActionResult} */ 20 const result = deserialize(await response.text()); 21 22 if (result.type === 'success') { 23 // 重新运行所有 `load` 函数,跟随成功的更新 24 await invalidateAll(); 25 } 26 27 applyAction(result); 28 } 29</script> 30 31<form method="POST" onsubmit={handleSubmit}> 32 <!-- content --> 33</form>
请注意,在使用 $app/forms 中相应的方法进一步处理响应之前,需要 deserialize 响应。仅 JSON.parse() 是不够的,因为表单 action(如 load 函数)也支持返回 Date 或 BigInt 对象。
如果您在 +page.server.js 旁边有一个 +server.js,fetch 请求将默认路由到那里。要改为 POST 到 +page.server.js 中的 action ,请使用自定义的 x-sveltekit-action 头:
1const response = await fetch(this.action, { 2 method: 'POST', 3 body: data, 4+++ headers: { 5 'x-sveltekit-action': 'true' 6 }+++ 7});
替代方案
表单 action 是向服务端发送数据的首选方法,因为它们可以渐进式增强,但您也可以使用 +server.js 文件来公开(例如)一个 JSON API。以下是这种交互的示例:
1<!--- file: src/routes/send-message/+page.svelte ---> 2<script> 3 function rerun() { 4 fetch('/api/ci', { 5 method: 'POST' 6 }); 7 } 8</script> 9 10<button onclick={rerun}>Rerun CI</button>
1// @errors: 2355 1360 2322 2/// file: src/routes/api/ci/+server.js 3/** @type {import('./$types').RequestHandler} */ 4export function POST() { 5 // do something 6}
GET 与 POST
如我们所见,要调用表单 action ,必须使用 method="POST"。
有些表单不需要向服务端 POST 数据 —— 例如搜索输入。对于这些表单,您可以使用 method="GET"(或等效地,不指定 method),SvelteKit 将像处理 <a> 元素一样处理它们,使用客户端路由而不是完整页面导航:
1<form action="/search"> 2 <label> 3 Search 4 <input name="q" /> 5 </label> 6</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
欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。
