Интеграция с Svelte

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;
  }
}

Разделение логики и UI

В Svelte важно отделять слой валидации от компонента интерфейса. Vest уже предоставляет изолированную функцию suite, поэтому рекомендуется выносить её в отдельный модуль.

Структура проекта:

/src
  /validation
    formValidation.js
  /components
    Form.svelte

Это позволяет переиспользовать одну и ту же схему валидации в разных компонентах без дублирования логики.


Использование результатов Vest в шаблоне 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

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}

Связь Vest с жизненным циклом компонентов Svelte

Svelte предоставляет хуки жизненного цикла, такие как onMount. Валидация может быть инициирована при инициализации формы:

import { onMount } from 'svelte';

onMount(() => {
  result = validationSuite(form);
});

Это особенно полезно при редактировании существующих данных, когда начальная валидация требуется сразу после загрузки.


Архитектурный паттерн интеграции

Наиболее устойчивый подход заключается в разделении на три слоя:

  • состояние формы (Svelte store или локальные переменные)
  • слой валидации (Vest suite)
  • отображение результатов (Svelte шаблон)

Такое разделение исключает смешивание ответственности и упрощает тестирование.

// state.js
export const form = writable({});
// validation.js
export const suite = create((data) => { ... });
<!-- Form.svelte -->
$: result = suite($form);