Form Actions 概念
什么是渐进增强?
SvelteKit 的 Form Actions 基于渐进增强(Progressive Enhancement)原则设计:表单在没有 JavaScript 的情况下仍然能够正常工作(标准 HTML 表单提交),当 JavaScript 加载后,use:enhance 指令将其升级为 AJAX 表单提交,获得更好的用户体验。
为什么渐进增强很重要?
网络质量差时 JS 可能未加载、用户可能禁用 JS、搜索引擎爬虫不执行 JS——渐进增强确保你的核心功能(登录、提交数据)在所有情况下都可用。
定义 Actions
// src/routes/login/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
export const actions: Actions = {
// default action:form 没有指定 action 属性时触发
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email')?.toString() || '';
const password = data.get('password')?.toString() || '';
// 表单验证
const errors: Record<string, string> = {};
if (!email) errors.email = '邮箱不能为空';
if (!/^[^@]+@[^@]+$/.test(email)) errors.email = '邮箱格式不正确';
if (password.length < 6) errors.password = '密码至少 6 个字符';
if (Object.keys(errors).length > 0) {
// fail() 返回错误,HTTP 状态码 400
// 原始表单数据也返回,方便前端回填
return fail(400, { errors, email });
}
// 验证用户
const user = await verifyUser(email, password);
if (!user) {
return fail(401, { errors: { global: '邮箱或密码错误' }, email });
}
// 设置 session cookie
cookies.set('session', await createSession(user.id), {
path: '/',
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7
});
// 重定向到仪表盘
redirect(303, '/dashboard');
}
};
多个命名 Actions
// src/routes/profile/+page.server.ts
export const actions: Actions = {
// 更新个人信息
updateProfile: async ({ request, locals }) => {
const data = await request.formData();
await db.user.update({
where: { id: locals.user.id },
data: { name: data.get('name') }
});
return { success: true, message: '个人信息已更新' };
},
// 上传头像
uploadAvatar: async ({ request, locals }) => {
const data = await request.formData();
const file = data.get('avatar') as File;
if (!file || file.size === 0) {
return fail(400, { error: '请选择文件' });
}
if (file.size > 5 * 1024 * 1024) {
return fail(400, { error: '文件大小不能超过 5MB' });
}
const url = await uploadToStorage(file);
await db.user.update({ where: { id: locals.user.id }, data: { avatarUrl: url } });
return { success: true, avatarUrl: url };
},
// 删除账户
deleteAccount: async ({ locals, cookies }) => {
await db.user.delete({ where: { id: locals.user.id } });
cookies.delete('session', { path: '/' });
redirect(303, '/');
}
};
use:enhance 渐进增强
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form } = $props<{ form: ActionData }>();
let loading = $state(false);
</script>
<form
method="POST"
use:enhance={({ formElement, formData, action, cancel }) => {
// 提交前回调
loading = true;
// 可以在这里修改 formData 或取消提交
// cancel() 阻止提交
return async ({ result, update }) => {
// 提交后回调
loading = false;
if (result.type === 'redirect') {
// 处理重定向
await update();
} else {
// 默认行为:更新 form 数据,失败时保留表单
await update();
}
};
}}
>
<div>
<label>
邮箱
<input type="email" name="email" value={form?.email || ''} />
</label>
{#if form?.errors?.email}
<p class="error">{form.errors.email}</p>
{/if}
</div>
<div>
<label>
密码
<input type="password" name="password" />
</label>
{#if form?.errors?.password}
<p class="error">{form.errors.password}</p>
{/if}
</div>
{#if form?.errors?.global}
<div class="error-banner">{form.errors.global}</div>
{/if}
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
<!-- 命名 action 的使用 -->
<form method="POST" action="?/uploadAvatar" use:enhance>
<input type="file" name="avatar" accept="image/*" />
<button type="submit">上传头像</button>
</form>
use:enhance 最简用法
直接写 use:enhance 不传参数,获得默认增强行为:AJAX 提交、自动更新 ActionData、失败时保留表单值。大多数场景下这已足够。
Form Actions 的工作机制
渐进增强的技术实现原理
SvelteKit Form Actions 实现渐进增强的关键在于:标准 HTML 表单和 AJAX 表单提交使用相同的服务端处理逻辑,差异只在于客户端如何处理响应:
不使用 use:enhance(纯 HTML 表单):
浏览器 → POST /login → 服务器执行 action → 返回 HTTP 302 重定向
浏览器跟随重定向,整页刷新
使用 use:enhance(AJAX 增强):
浏览器 → fetch POST /login → 服务器执行同一个 action → 返回 JSON
Svelte 更新 $page.form 和页面 data,无整页刷新
关键点:
服务器的 action 函数完全相同——不需要区分两种情况。
SvelteKit 在响应时会检测请求头中的 Accept 字段:
- 普通表单:Accept: text/html → 返回重定向
- use:enhance:Accept: application/json → 返回 JSON
action 函数的返回值类型
fail(status, data) — 验证失败
返回
fail(400, { errors: { email: '格式不正确' } }) 时,HTTP 状态码为 400,ActionData(form)会被更新为 data 内容,表单不重置(用户可以修正并重新提交)。这是表单验证失败的标准处理。redirect(status, url) — 成功跳转
返回
redirect(303, '/dashboard') 时,用户被重定向到目标页面(303 See Other 是 POST 后重定向的正确状态码,防止浏览器重复提交)。注意:redirect 在 SvelteKit 内部通过抛出异常实现,应该直接 throw redirect(303, '/dashboard')。返回普通对象 — 成功但不跳转
返回
{ success: true, message: '已保存' } 时,页面不刷新,ActionData 更新为该对象,可以在组件中读取 form?.success 来显示成功提示。适合"原地操作"(如点赞、添加评论不需要跳转)。什么都不返回(void)
如果 action 什么都不返回,ActionData 保持为 null,通常配合 redirect 使用:执行操作后重定向到另一页面,不需要在当前页面显示任何状态。
表单 CSRF 保护
SvelteKit 的 Form Actions 内置了 CSRF(跨站请求伪造)防护,了解其工作原理有助于正确配置:
// SvelteKit 默认在所有 Action 上启用 CSRF 保护:
// 1. 检查请求的 Origin 头部是否与服务器域名匹配
// 2. 如果不匹配(跨域 POST),拒绝请求(403 Forbidden)
// 如果需要允许跨域的 form 提交(少见),在 svelte.config.js 中配置:
import { sveltekit } from '@sveltejs/kit/vite';
export default {
kit: {
csrf: {
// 警告:禁用 CSRF 保护会引入安全风险
checkOrigin: false // 只在完全理解后果时才禁用
}
}
};
action 中不要忘记验证数据类型
表单提交的所有数据都是字符串类型(HTML 表单不传递类型信息)。即使 input 是 type="number",formData.get('price') 返回的也是字符串 "99.9" 而不是数字 99.9。始终在 action 中做明确的类型转换和验证。推荐使用 Zod 或 Valibot 进行 Schema 验证。
Zod Schema 验证集成
使用 Zod 在 Form Action 中进行类型安全的数据验证是生产级应用的标准做法:
// src/routes/products/new/+page.server.ts
import { z } from 'zod';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
// 定义数据验证 Schema
const productSchema = z.object({
name: z.string().min(2, '名称至少2个字符').max(100),
price: z.coerce.number().positive('价格必须大于0'), // coerce 自动转换字符串→数字
category: z.enum(['electronics', 'clothing', 'food']),
description: z.string().max(1000).optional(),
inStock: z.coerce.boolean().default(true)
});
export const actions: Actions = {
create: async ({ request, locals }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
const formData = await request.formData();
const rawData = Object.fromEntries(formData);
// Zod 验证:parse 失败时抛出异常,safeParse 返回结果对象
const parsed = productSchema.safeParse(rawData);
if (!parsed.success) {
// 将 Zod 错误转换为字段级别的错误 map
const errors = Object.fromEntries(
Object.entries(parsed.error.flatten().fieldErrors)
.map(([field, msgs]) => [field, msgs?.[0] ?? '验证失败'])
);
return fail(400, {
errors,
values: rawData // 回填用户输入(避免重新填写)
});
}
// 类型安全:parsed.data 是完整的 Product 类型
await db.product.create({ data: { ...parsed.data, userId: locals.user.id } });
throw redirect(303, '/products');
}
};
<!-- +page.svelte:显示字段级别错误 -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form } = $props<{ form: ActionData }>();
let submitting = $state(false);
</script>
<form method="POST" action="?/create" use:enhance={() => {
submitting = true;
return async ({ update }) => {
submitting = false;
await update();
};
}}>
<div>
<label>产品名称</label>
<input name="name" value={form?.values?.name ?? ''}/>
<!-- 显示字段错误 -->
{#if form?.errors?.name}
<span class="error">{form.errors.name}</span>
{/if}
</div>
<div>
<label>价格</label>
<input type="number" name="price" value={form?.values?.price ?? ''}/>
{#if form?.errors?.price}
<span class="error">{form.errors.price}</span>
{/if}
</div>
<button type="submit" disabled={submitting}>
{submitting ? '提交中...' : '创建产品'}
</button>
</form>
本章小结
- 渐进增强原则:action 函数完全相同,use:enhance 通过检测 Accept 头部决定返回重定向还是 JSON;核心逻辑只写一次
- 命名 actions(
?/login、?/register)允许一个页面有多个独立的表单处理端点,比单独的 API 路由更简洁 - 四种 action 返回值:fail(验证失败保留表单)、redirect(成功跳转)、普通对象(成功不跳转)、void(配合 redirect)
- use:enhance 回调可以在提交前后添加逻辑:loading 状态、乐观更新、自定义成功/失败处理
- 表单数据全是字符串:始终在 action 中做类型转换和验证(推荐 Zod Schema)
- CSRF 保护内置:SvelteKit 自动检查 Origin 头,不需要额外的 token 机制;禁用会带来安全风险