全书目录

第三十篇:SvelteKit 完全指南 —— 轻量反应式剧场宇宙

第十章:`actions` —— 票务窗口才是 SvelteKit 最有味道的地方

1 分钟 393 字 第 548 / 962 个阅读单元

很多框架讲“表单提交”,默认思路是:

text
阻止默认提交
自己收集数据
自己 fetch
自己处理 loading/error
自己跳转

SvelteKit 的气质不是这样。

它更像一座真正的剧院票务窗口:

text
观众提交表单
窗口直接受理
失败就把退回理由写在单子上
成功就给新票据或引导去下一站

这就是 form actions

它建立在很朴素的 Web 标准之上:

  • 原生 <form>
  • 原生 POST
  • 服务端 actions
  • 可选的 use:enhance 做渐进增强

这点非常关键:

先让它在“没有 JavaScript”的情况下也成立,再考虑增强体验。

这就是 SvelteKit 的剧场哲学:
先保证演出能演,再让机关更丝滑。

看一个完整例子。

ts
// src/routes/box-office/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { db } from '$lib/server/db';

export const actions: Actions = {
  default: async ({ request, locals }) => {
    if (!locals.user) {
      redirect(303, '/login');
    }

    const formData = await request.formData();

    const playSlug = String(formData.get('playSlug') ?? '').trim();
    const buyerName = String(formData.get('buyerName') ?? '').trim();
    const seats = Number(formData.get('seats') ?? 0);

    const errors: Record<string, string> = {};

    if (!playSlug) errors.playSlug = '请选择剧目';
    if (!buyerName) errors.buyerName = '请输入订票人姓名';
    if (!Number.isInteger(seats) || seats < 1) errors.seats = '至少订 1 张票';

    if (Object.keys(errors).length > 0) {
      return fail(400, {
        errors,
        values: { playSlug, buyerName, seats }
      });
    }

    const order = await db.ticketOrder.create({
      data: {
        playSlug,
        buyerName,
        seats,
        userId: locals.user.id
      }
    });

    redirect(303, `/orders/${order.id}`);
  }
};
svelte
<!-- src/routes/box-office/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageProps } from './$types';

  let { form }: PageProps = $props();
  let pending = $state(false);
</script>

<h1>票务窗口</h1>

<form
  method="POST"
  use:enhance={() => {
    pending = true;

    return async () => {
      pending = false;
    };
  }}
>
  <label>
    剧目代号
    <input name="playSlug" value={form?.values?.playSlug ?? ''} />
  </label>
  {#if form?.errors?.playSlug}
    <p class="error">{form.errors.playSlug}</p>
  {/if}

  <label>
    订票人
    <input name="buyerName" value={form?.values?.buyerName ?? ''} />
  </label>
  {#if form?.errors?.buyerName}
    <p class="error">{form.errors.buyerName}</p>
  {/if}

  <label>
    票数
    <input name="seats" type="number" min="1" value={form?.values?.seats ?? 1} />
  </label>
  {#if form?.errors?.seats}
    <p class="error">{form.errors.seats}</p>
  {/if}

  <button disabled={pending}>
    {pending ? '出票中...' : '确认订票'}
  </button>
</form>

这套模式的好处非常大:

  • 没有 JS 也能提交
  • 服务端校验天然可靠
  • fail(...) 返回的字段错误会回到同一页
  • redirect(...) 是请求级跳转,不是你手搓的导航逻辑
  • use:enhance 只是增强体验,不是业务成立的前提

这比“手写一堆 fetch 提交流程”更稳、更贴近 Web,也更适合表单密集型系统。