Svelte реализует компиляционный подход к построению интерфейсов, при котором реактивность встраивается в результат сборки, а не выполняется в рантайме через виртуальное дерево. Vest, в свою очередь, представляет декларативную систему валидации, основанную на тест-кейсах и ленивом выполнении проверок. Эти две системы естественно дополняют друг друга: Svelte управляет состоянием и реактивностью, Vest управляет правилами проверки данных и их структурой.
Ключевой момент интеграции заключается в том, что Svelte обеспечивает реактивное хранение значений формы, а Vest использует эти значения как входные данные для выполнения набора валидирующих тестов. При изменении состояния формы Vest может быть повторно вызван, а результаты валидации становятся частью реактивного состояния Svelte.
Типичный сценарий включает форму с реактивными переменными Svelte и функцию валидации Vest, которая вызывается при изменении этих переменных.
import { create, test, enforce } from 'vest';
const validationSuite = create((data = {}) => {
test('username', 'Имя пользователя обязательно', () => {
enforce(data.username).isNotEmpty();
});
test('email', 'Некорректный email', () => {
enforce(data.email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});
});
В Svelte-компоненте данные формы становятся реактивными:
let form = {
username: '',
email: ''
};
При изменении любого поля запускается повторная валидация:
import { validationSuite } from './validation';
let result;
$: result = validationSuite(form);
Svelte автоматически отслеживает зависимость от form, и
при любом изменении выполняет пересчёт result, что делает
интеграцию прямолинейной и прозрачной.
Svelte предоставляет двустороннюю привязку через директиву
bind:value, что позволяет синхронизировать DOM и состояние
без дополнительных обработчиков.
<input bind:value={form.username} />
<input bind:value={form.email} />
Каждое изменение поля приводит к обновлению объекта
form, а следовательно — к повторному запуску Vest-валидации
через реактивное выражение.
Важно учитывать, что такая схема подходит для небольших и средних форм. При сложных формах с большим количеством полей желательно контролировать частоту запуска валидации.
Поскольку Vest выполняет набор тестов при каждом вызове, чрезмерная реактивность может привести к избыточным вычислениям. В Svelte это решается через явное управление триггером валидации.
Один из распространённых подходов — использование локального состояния “touched” или “dirty”:
let touched = false;
function handleInput() {
touched = true;
}
И условный запуск:
$: result = touched ? validationSuite(form) : null;
Таким образом валидация не выполняется до первого взаимодействия пользователя с формой.
Vest поддерживает асинхронные тесты, что позволяет интегрировать проверку серверных данных, например уникальности логина.
test('username_unique', 'Имя уже занято', async () => {
const response = await fetch(`/api/check?username=${data.username}`);
const { exists } = await response.json();
enforce(exists).isFalsy();
});
В Svelte асинхронные результаты требуют аккуратного управления состоянием, поскольку реактивное выражение не ожидает завершения Promise.
let result;
$: (async () => {
result = await validationSuite(form);
})();
Однако такой подход может приводить к гонкам состояний при быстром вводе. Более контролируемая схема — хранение версии запроса:
let version = 0;
async function validate() {
const current = ++version;
const res = await validationSuite(form);
if (current === version) {
result = res;
}
}
В Svelte важно отделять слой валидации от компонента интерфейса. Vest уже предоставляет изолированную функцию suite, поэтому рекомендуется выносить её в отдельный модуль.
Структура проекта:
/src
/validation
formValidation.js
/components
Form.svelte
Это позволяет переиспользовать одну и ту же схему валидации в разных компонентах без дублирования логики.
Результат выполнения suite содержит структурированную информацию о
статусе полей и сообщений об ошибках. Обычно он обрабатывается через
методы hasErrors, getErrors,
isValid.
$: isValid = result?.isValid();
$: usernameErrors = result?.getErrors('username');
В шаблоне:
{#if usernameErrors}
<ul>
{#each usernameErrors as error}
<li>{error}</li>
{/each}
</ul>
{/if}
Такой подход сохраняет декларативность Svelte и позволяет не дублировать проверочную логику в шаблоне.
Vest поддерживает композицию, что особенно полезно в Svelte-приложениях с переиспользуемыми формами.
const baseSuite = create((data) => {
test('email', () => {
enforce(data.email).isNotEmpty();
});
});
const extendedSuite = create((data) => {
baseSuite(data);
test('password', () => {
enforce(data.password).longerThan(6);
});
});
В Svelte используется единая точка вызова:
$: result = extendedSuite(form);
Основная проблема при интеграции — избыточные пересчёты. Svelte реагирует на любое изменение зависимостей, поэтому даже изменение одного поля приводит к полной валидации всей формы.
Оптимизация достигается через частичную валидацию:
const suite = create((data, field) => {
test(field === 'email' ? 'email' : null, () => {
if (field !== 'email') return;
enforce(data.email).isNotEmpty();
});
});
И вызов:
$: result = suite(form, changedField);
Это снижает нагрузку при больших формах.
Svelte stores позволяют вынести состояние формы и результат валидации в отдельный реактивный слой.
import { writable, derived } from 'svelte/store';
export const form = writable({
username: '',
email: ''
});
Валидация через derived store:
import { validationSuite } from './validation';
export const validation = derived(form, ($form) => {
return validationSuite($form);
});
Такой подход особенно удобен при масштабировании приложения, так как отделяет бизнес-логику от компонентов.
В крупных формах важно нормализовать структуру ошибок. Vest возвращает ошибки по полям, но в Svelte часто требуется преобразование в более удобный формат.
function mapErrors(result) {
return result.getErrors() || {};
}
Использование:
$: errors = mapErrors(result);
Шаблон:
{#if errors.email}
<span>{errors.email[0]}</span>
{/if}
При быстром вводе данных важно избегать ситуации, когда UI отображает устаревшие ошибки. Решение заключается в синхронной привязке состояния формы и асинхронной обработке результата.
let pending = false;
async function validate() {
pending = true;
const res = await validationSuite(form);
result = res;
pending = false;
}
В интерфейсе:
{#if pending}
<span>Проверка...</span>
{/if}
Svelte предоставляет хуки жизненного цикла, такие как
onMount. Валидация может быть инициирована при
инициализации формы:
import { onMount } from 'svelte';
onMount(() => {
result = validationSuite(form);
});
Это особенно полезно при редактировании существующих данных, когда начальная валидация требуется сразу после загрузки.
Наиболее устойчивый подход заключается в разделении на три слоя:
Такое разделение исключает смешивание ответственности и упрощает тестирование.
// state.js
export const form = writable({});
// validation.js
export const suite = create((data) => { ... });
<!-- Form.svelte -->
$: result = suite($form);