SvelteKit 最新中文文档教程(4)—— 表单 actions
<h2 id="前言">前言</h2><p>Svelte,一个语法简洁、入门容易,面向未来的前端框架。</p>
<p>从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,<strong>从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1</strong>:</p>
<p><img src="https://yayujs-blog.oss-cn-beijing.aliyuncs.com/405488775-48df16b1-939c-489b-8d52-6071869893f0.png"></p>
<p>Svelte 以其独特的编译时优化机制著称,具有<strong>轻量级</strong>、<strong>高性能</strong>、<strong>易上手</strong>等特性,<strong>非常适合构建轻量级 Web 项目</strong>。</p>
<p>为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。</p>
<p>如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p>
<h2 id="表单-actions">表单 actions</h2>
<p><code>+page.server.js</code> 文件可以导出 <em>actions</em>,允许您使用 <code><form></code> 元素向服务端 <code>POST</code> 数据。</p>
<p>使用 <code><form></code> 时,客户端 JavaScript 是可选的,但您可以轻松地使用 JavaScript <em>渐进式增强</em> 表单交互,以提供最佳的用户体验。</p>
<h2 id="默认-action">默认 action</h2>
<p>在最简单的情况下,一个页面声明一个 <code>default</code> action:</p>
<pre><code class="language-js">/// file: src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async (event) => {
// TODO log the user in
}
};
</code></pre>
<p>要从 <code>/login</code> 页面调用此 action,只需添加一个 <code><form></code> —— 不需要 JavaScript:</p>
<pre><code class="language-svelte"><!--- file: src/routes/login/+page.svelte --->
<form method="POST">
<label>
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>
</code></pre>
<p>如果有人点击按钮,浏览器将通过 <code>POST</code> 请求将表单数据发送到服务端,运行默认 action。</p>
<blockquote>
<p>[!NOTE] action 总是使用 <code>POST</code> 请求,因为 <code>GET</code> 请求不应该有副作用。</p>
</blockquote>
<p>我们还可以通过添加 <code>action</code> 属性,调用来自其他页面的 action (例如,如果根布局中的导航栏有一个登录小部件):</p>
<pre><code class="language-html">/// file: src/routes/+layout.svelte
<form method="POST" action="/login">
<!-- content -->
</form>
</code></pre>
<h2 id="命名-actions">命名 actions</h2>
<p>页面可以根据需要拥有多个命名 action ,而不是只有一个 <code>default</code> action:</p>
<pre><code class="language-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
}+++
};
</code></pre>
<p>要调用命名 action ,添加一个以 <code>/</code> 字符为前缀的查询参数:</p>
<pre><code class="language-svelte"><!--- file: src/routes/login/+page.svelte --->
<form method="POST" action="?/register">
</code></pre>
<pre><code class="language-svelte"><!--- file: src/routes/+layout.svelte --->
<form method="POST" action="/login?/register">
</code></pre>
<p>除了 <code>action</code> 属性,我们还可以在按钮上使用 <code>formaction</code> 属性,将相同的表单数据 <code>POST</code> 到与父 <code><form></code> 不同的 action :</p>
<pre><code class="language-svelte">/// file: src/routes/login/+page.svelte
<form method="POST" +++action="?/login"+++>
<label>
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
+++<button formaction="?/register">Register</button>+++
</form>
</code></pre>
<blockquote>
<p>[!NOTE] 我们不能在命名 action 旁边有默认 action ,因为如果您在没有重定向的情况下 <code>POST</code> 到命名 action ,查询参数会保留在 URL 中,这意味着下一个默认 <code>POST</code> 将通过之前的命名 action 进行处理。</p>
</blockquote>
<h2 id="action-的结构">action 的结构</h2>
<p>每个 action 接收一个 <code>RequestEvent</code> 对象,允许您使用 <code>request.formData()</code> 读取数据。在处理请求之后(例如,通过设置 cookie 让用户登录),action 可以响应数据,这些数据将在对应页面的 <code>form</code> 属性以及整个应用范围的 <code>page.form</code> 中可用,直到下一次更新。</p>
<pre><code class="language-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
}
};
</code></pre>
<pre><code class="language-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}
</code></pre>
<blockquote>
<p>[!LEGACY]<br>
在 Svelte 4 中,您将使用 <code>export let data</code> 和 <code>export let form</code> 来声明属性</p>
</blockquote>
<h3 id="验证错误">验证错误</h3>
<p>如果请求因数据无效而无法处理,您可以将验证错误 —— 以及之前提交的表单值 —— 返回给用户,以便他们可以重试。<code>fail</code> 函数允许您返回一个 HTTP 状态码(通常是 400 或 422,用于验证错误)以及数据。状态码可以通过 <code>page.status</code> 获取,数据可以通过 <code>form</code> 获取:</p>
<pre><code class="language-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
}
};
</code></pre>
<blockquote>
<p>[!NOTE] 请注意,作为预防措施,我们只将电子邮件返回给页面 —— 而不是密码。</p>
</blockquote>
<pre><code class="language-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>
<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>
</code></pre>
<p>返回的数据必须可序列化为 JSON。除此之外,结构完全由您决定。例如,如果页面上有多个表单,您可以使用 <code>id</code> 属性或类似的方式区分返回的 <code>form</code> 数据对应哪个 <code><form></code>。</p>
<h3 id="重定向">重定向</h3>
<p>重定向(和错误)与 <code>load</code> 中的工作方式完全相同:</p>
<pre><code class="language-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
}
};
</code></pre>
<h2 id="加载数据">加载数据</h2>
<p>action 运行后,页面将重新渲染(除非发生重定向或意外错误), action 的返回值将作为 <code>form</code> 属性提供给页面。这意味着页面的 <code>load</code> 函数将在 action 完成后运行。</p>
<p>请注意,<code>handle</code> 在 action 被调用之前运行,并且不会在 <code>load</code> 函数之前重新运行。这意味着,例如,如果您使用 <code>handle</code> 根据 cookie 填充 <code>event.locals</code>,则在 action 中设置或删除 cookie 时,必须更新 <code>event.locals</code>:</p>
<pre><code class="language-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);
}
</code></pre>
<pre><code class="language-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;
}
};
</code></pre>
<h2 id="渐进式增强">渐进式增强</h2>
<p>在前面的章节中,我们构建了一个在没有客户端 JavaScript 的情况下工作的 <code>/login</code> action —— 没有 <code>fetch</code>。这很好,但当 JavaScript <em>可用</em> 时,我们可以渐进式增强表单交互,以提供更好的用户体验。</p>
<h3 id="useenhance">use:enhance</h3>
<p>渐进式增强表单的最简单方法是添加 <code>use:enhance</code> action :</p>
<pre><code class="language-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+++>
</code></pre>
<blockquote>
<p>[!NOTE] <code>use:enhance</code> 只能与 <code>method="POST"</code> 的表单一起使用。它将无法与 <code>method="GET"</code> 一起工作,后者是未指定方法的表单的默认方法。在未指定 <code>method="POST"</code> 的表单上尝试使用 <code>use:enhance</code> 将导致错误。</p>
</blockquote>
<blockquote>
<p>[!NOTE] 是的,<code>enhance</code> action 和 <code><form action></code> 都叫做 'action',这些文档充满了各种 action。抱歉。</p>
</blockquote>
<p>没有参数时,<code>use:enhance</code> 将模拟浏览器原生行为,只是不进行完整页面重载。它将:</p>
<ul>
<li>在成功或无效响应时更新 <code>form</code> 属性、<code>page.form</code> 和 <code>page.status</code>,但仅当 action 在您提交的同一页面上时。例如,如果您的表单看起来像 <code><form action="/somewhere/else" ..></code>,<code>form</code> 属性和 <code>page.form</code> 状态将 <em>不会</em> 更新。这是因为在本地表单提交的情况下,您将被重定向到 action 所在的页面。如果您希望无论如何都能更新,使用 <code>applyAction</code></li>
<li>重置 <code><form></code> 元素</li>
<li>在成功响应时使用 <code>invalidateAll</code> 使所有数据失效</li>
<li>在重定向响应时调用 <code>goto</code></li>
<li>如果发生错误,渲染最近的 <code>+error</code> 边界</li>
<li>将焦点重置到适当的元素</li>
</ul>
<h3 id="自定义-useenhance">自定义 use:enhance</h3>
<p>要自定义行为,您可以提供一个 <code>SubmitFunction</code>,它会在表单提交前立即运行,并(可选地)返回一个随 <code>ActionResult</code> 一起运行的回调。请注意,如果您返回一个回调,上述默认行为将不会被触发。要恢复默认行为,请调用 <code>update</code>。</p>
<pre><code class="language-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` 是一个触发默认逻辑的函数,如果没有设置此回调
};
}}
>
</code></pre>
<p>您可以使用这些函数来显示和隐藏加载界面等。</p>
<p>如果您返回一个回调,您可能需要重现部分默认的 <code>use:enhance</code> 行为,但在成功响应时不使所有数据失效。您可以使用 <code>applyAction</code> 来实现:</p>
<pre><code class="language-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);
}+++
};
}}
>
</code></pre>
<p><code>applyAction(result)</code> 的行为取决于 <code>result.type</code>:</p>
<ul>
<li><code>success</code>, <code>failure</code> — 将 <code>page.status</code> 设置为 <code>result.status</code>,并将 <code>form</code> 和 <code>page.form</code> 更新为 <code>result.data</code>(无论您从哪里提交,这与 <code>enhance</code> 的 <code>update</code> 形成对比)</li>
<li><code>redirect</code> — 调用 <code>goto(result.location, { invalidateAll: true })</code></li>
<li><code>error</code> — 使用 <code>result.error</code> 渲染最近的 <code>+error</code> 边界</li>
</ul>
<p>在所有情况下,焦点将被重置。</p>
<h3 id="自定义事件监听器">自定义事件监听器</h3>
<p>我们也可以不使用 <code>use:enhance</code>,在 <code><form></code> 上使用普通的事件监听器,自己实现渐进式增强:</p>
<pre><code class="language-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>
</code></pre>
<p>请注意,在使用 <code>$app/forms</code> 中相应的方法进一步处理响应之前,需要 <code>deserialize</code> 响应。仅 <code>JSON.parse()</code> 是不够的,因为表单 action(如 <code>load</code> 函数)也支持返回 <code>Date</code> 或 <code>BigInt</code> 对象。</p>
<p>如果您在 <code>+page.server.js</code> 旁边有一个 <code>+server.js</code>,<code>fetch</code> 请求将默认路由到那里。要改为 <code>POST</code> 到 <code>+page.server.js</code> 中的 action ,请使用自定义的 <code>x-sveltekit-action</code> 头:</p>
<pre><code class="language-js">const response = await fetch(this.action, {
method: 'POST',
body: data,
+++ headers: {
'x-sveltekit-action': 'true'
}+++
});
</code></pre>
<h2 id="替代方案">替代方案</h2>
<p>表单 action 是向服务端发送数据的首选方法,因为它们可以渐进式增强,但您也可以使用 <code>+server.js</code> 文件来公开(例如)一个 JSON API。以下是这种交互的示例:</p>
<pre><code class="language-svelte"><!--- file: src/routes/send-message/+page.svelte --->
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button onclick={rerun}>Rerun CI</button>
</code></pre>
<pre><code class="language-js">// @errors: 2355 1360 2322
/// file: src/routes/api/ci/+server.js
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}
</code></pre>
<h2 id="get-与-post">GET 与 POST</h2>
<p>如我们所见,要调用表单 action ,必须使用 <code>method="POST"</code>。</p>
<p>有些表单不需要向服务端 <code>POST</code> 数据 —— 例如搜索输入。对于这些表单,您可以使用 <code>method="GET"</code>(或等效地,不指定 <code>method</code>),SvelteKit 将像处理 <code><a></code> 元素一样处理它们,使用客户端路由而不是完整页面导航:</p>
<pre><code class="language-html"><form action="/search">
<label>
Search
<input name="q" />
</label>
</form>
</code></pre>
<p>提交此表单将导航到 <code>/search?q=...</code> 并调用您的 <code>load</code> 函数,但不会调用 action 。与 <code><a></code> 元素一样,您可以在 <code><form></code> 上设置 <code>data-sveltekit-reload</code>、<code>data-sveltekit-replacestate</code>、<code>data-sveltekit-keepfocus</code> 以及 <code>data-sveltekit-noscroll</code> 属性,以控制路由器的行为。</p>
<h2 id="进一步阅读">进一步阅读</h2>
<ul>
<li>教程:表单</li>
</ul>
<h2 id="svelte-中文文档">Svelte 中文文档</h2>
<p>点击查看中文文档 - SvelteKit 表单 actions。</p>
<p>系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p><br><br>
来源:https://www.cnblogs.com/yayujs/p/18777526
頁:
[1]