SvelteKit 最新中文文档教程(4)—— 表单 actions

前言

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 和 `

` 都叫做 'action',这些文档充满了各种 action。抱歉。

没有参数时,use:enhance 将模拟浏览器原生行为,只是不进行完整页面重载。它将:

  • 在成功或无效响应时更新 form 属性、page.formpage.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,并将 formpage.form 更新为 result.data(无论您从哪里提交,这与 enhanceupdate 形成对比)
  • 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 函数)也支持返回 DateBigInt 对象。

如果您在 +page.server.js 旁边有一个 +server.jsfetch 请求将默认路由到那里。要改为 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-reloaddata-sveltekit-replacestatedata-sveltekit-keepfocus 以及 data-sveltekit-noscroll 属性,以控制路由器的行为。

进一步阅读

Svelte 中文文档

点击查看中文文档 - SvelteKit 表单 actions

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng...

欢迎围观我的"网页版朋友圈"、加入"冴羽·成长陪伴社群",踏上"前端大佬成长之路"

相关推荐
NaZiMeKiY8 分钟前
HTML5前端第二章节
前端·html·html5
天若有情67311 分钟前
深入浅出:HTML 中 <a> 标签嵌入链接教程
前端·html
烂蜻蜓12 分钟前
HTML 样式之 CSS 全面解析
前端·css·html
冬冬小圆帽13 分钟前
Webpack 优化深度解析:从构建性能到输出优化的全面指南
前端·webpack·node.js
大龄大专大前端2 小时前
JavaScript闭包的认识/应用/原理
前端·javascript·ecmascript 6
字节源流2 小时前
【SpringMVC】常用注解:@SessionAttributes
java·服务器·前端
肥肠可耐的西西公主2 小时前
前端(vue)学习笔记(CLASS 4):组件组成部分与通信
前端·vue.js·学习
烛阴2 小时前
JavaScript 函数绑定:从入门到精通,解锁你的代码超能力!
前端·javascript
花椒和蕊3 小时前
【vue+excel】导出excel(目前是可以导出两个sheet)
javascript·vue.js·excel
泫凝3 小时前
使用 WebP 优化 GPU 纹理占用
前端·javascript