用户是一群淘气鬼,如果有机会,他们会提交各种无意义的数据。为了防止它们造成混乱,验证表单数据非常重要。
¥Users are a mischievous bunch, who will submit all kinds of nonsensical data if given the chance. To prevent them from causing chaos, it’s important to validate form data.
第一道防线是浏览器的 内置表单验证,这使得将 <input>
标记为必需变得容易:
¥The first line of defense is the browser’s built-in form validation, which makes it easy to, for example, mark an <input>
as required:
<form method="POST" action="?/create">
<label>
add a todo
<input
name="description"
autocomplete="off"
required
/>
</label>
</form>
尝试在 <input>
为空时按 Enter。
¥Try hitting Enter while the <input>
is empty.
这种验证很有帮助,但还不够。某些验证规则(例如唯一性)无法使用 <input>
属性来表达,无论如何,如果用户是精英黑客,他们可能会简单地使用浏览器的 devtools 删除属性。为了防止这些恶作剧,你应该始终使用服务器端验证。
¥This kind of validation is helpful, but insufficient. Some validation rules (e.g. uniqueness) can’t be expressed using <input>
attributes, and in any case, if the user is an elite hacker they might simply delete the attributes using the browser’s devtools. To guard against these sorts of shenanigans, you should always use server-side validation.
在 src/lib/server/database.js
中,验证描述是否存在且唯一:
¥In src/lib/server/database.js
, validate that the description exists and is unique:
export function createTodo(userid, description) {
if (description === '') {
throw new Error('todo must have a description');
}
const todos = db.get(userid);
if (todos.find((todo) => todo.description === description)) {
throw new Error('todos must be unique');
}
todos.push({
id: crypto.randomUUID(),
description,
done: false
});
}
尝试提交重复的待办事项。哎呀!SvelteKit 将我们带到一个看起来不友好的错误页面。在服务器上,我们看到 ‘待办事项必须是唯一的’ 错误,但 SvelteKit 会向用户隐藏意外的错误消息,因为它们通常包含敏感数据。
¥Try submitting a duplicate todo. Yikes! SvelteKit takes us to an unfriendly-looking error page. On the server, we see a ‘todos must be unique’ error, but SvelteKit hides unexpected error messages from users because they often contain sensitive data.
最好停留在同一页面上,并提供出错的指示,以便用户可以修复它。为此,我们可以使用 fail
函数从操作中返回数据以及适当的 HTTP 状态代码:
¥It would be much better to stay on the same page and provide an indication of what went wrong so that the user can fix it. To do this, we can use the fail
function to return data from the action along with an appropriate HTTP status code:
import { fail } from '@sveltejs/kit';
import * as db from '$lib/server/database.js';
export function load({ cookies }) {...}
export const actions = {
create: async ({ cookies, request }) => {
const data = await request.formData();
try {
db.createTodo(cookies.get('userid'), data.get('description'));
} catch (error) {
return fail(422, {
description: data.get('description'),
error: error.message
});
}
}
在 src/routes/+page.svelte
中,我们可以通过 form
属性访问返回值,该属性仅在表单提交后填充:
¥In src/routes/+page.svelte
, we can access the returned value via the form
prop, which is only ever populated after a form submission:
<script>
let { data, form } = $props();
</script>
<div class="centered">
<h1>todos</h1>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST" action="?/create">
<label>
add a todo:
<input
name="description"
value={form?.description ?? ''}
autocomplete="off"
required
/>
</label>
</form>
<script lang="ts">
let { data, form } = $props();
</script>
<div class="centered">
<h1>todos</h1>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST" action="?/create">
<label>
add a todo:
<input
name="description"
value={form?.description ?? ''}
autocomplete="off"
required
/>
</label>
</form>
你还可以从操作返回数据而不将其封装在
fail
中 - 例如在保存数据时显示 ‘成功!’ 消息 - 并且它将通过form
prop 提供。¥[!NOTE] You can also return data from an action without wrapping it in
fail
— for example to show a ‘success!’ message when data was saved — and it will be available via theform
prop.
<script>
let { data } = $props();
</script>
<div class="centered">
<h1>todos</h1>
<form method="POST" action="?/create">
<label>
add a todo:
<input
name="description"
autocomplete="off"
/>
</label>
</form>
<ul class="todos">
{#each data.todos as todo (todo.id)}
<li>
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={todo.id} />
<span>{todo.description}</span>
<button aria-label="Mark as complete"></button>
</form>
</li>
{/each}
</ul>
</div>
<style>
.centered {
max-width: 20em;
margin: 0 auto;
}
label {
width: 100%;
}
input {
flex: 1;
}
span {
flex: 1;
}
button {
border: none;
background: url(./remove.svg) no-repeat 50% 50%;
background-size: 1rem 1rem;
cursor: pointer;
height: 100%;
aspect-ratio: 1;
opacity: 0.5;
transition: opacity 0.2s;
}
button:hover {
opacity: 1;
}
.saving {
opacity: 0.5;
}
</style>