Асинхронная валидация используется в случаях, когда проверка данных требует обращения к внешнему источнику:
Типичные сценарии:
В библиотеке Vest асинхронная валидация встроена в архитектуру suites и тесно связана с механизмами:
testenforceasyncwarnskipWhenonlygroupoptionalПростейшая асинхронная проверка строится вокруг async и
await.
import vest, { test, enforce } from 'vest';
const suite = vest.create(async (data) => {
test('email', 'Email уже используется', async () => {
const response = await fetch(`/api/check-email?email=${data.email}`);
const result = await response.json();
enforce(result.available).isTruthy();
});
});
Особенности:
test становится асинхронным;Весь suite также может быть асинхронным.
const suite = vest.create(async (data) => {
await preloadConfiguration();
test('username', async () => {
const result = await checkUsername(data.username);
enforce(result.valid).isTruthy();
});
});
Это полезно, если:
Один из наиболее распространённых сценариев.
import vest, { test, enforce } from 'vest';
async function isEmailAvailable(email) {
const response = await fetch('/api/email/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
return response.json();
}
const suite = vest.create((data) => {
test('email', 'Email уже зарегистрирован', async () => {
const result = await isEmailAvailable(data.email);
enforce(result.available).isTruthy();
});
});
Vest предоставляет API для анализа состояния валидации.
const result = suite(formData);
result.isPending();
result.hasErrors();
result.hasWarnings();
Проверка pending-состояния:
if (result.isPending()) {
console.log('Проверка выполняется');
}
Проверка конкретного поля:
result.isPending('email');
Это особенно важно для UI:
Vest способен выполнять несколько асинхронных тестов параллельно.
const suite = vest.create((data) => {
test('email', async () => {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
});
test('username', async () => {
const result = await checkUsername(data.username);
enforce(result.available).isTruthy();
});
test('promoCode', async () => {
const result = await validatePromo(data.promoCode);
enforce(result.valid).isTruthy();
});
});
Все проверки запускаются независимо.
Преимущества:
Иногда проверки должны выполняться последовательно.
Например:
const suite = vest.create((data) => {
test('email', 'Некорректный email', () => {
enforce(data.email).matches(/^\S+@\S+\.\S+$/);
});
test('email', 'Email уже существует', async () => {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
});
});
Однако такой вариант всё ещё может выполнять второй тест.
Для предотвращения лишнего запроса применяется
skipWhen.
import vest, {
create,
test,
skipWhen,
enforce
} from 'vest';
const suite = create((data) => {
test('email', 'Неверный email', () => {
enforce(data.email).matches(/^\S+@\S+\.\S+$/);
});
skipWhen(
res => res.hasErrors('email'),
() => {
test('email', 'Email занят', async () => {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
});
}
);
});
Теперь API-запрос выполняется только при успешной синхронной проверке.
Асинхронные проверки часто вызываются при вводе текста.
Без debounce возникают проблемы:
Пример debounce:
import debounce from 'lodash.debounce';
const validateUsername = debounce(async (value) => {
return await checkUsername(value);
}, 300);
Использование:
test('username', async () => {
const result = await validateUsername(data.username);
enforce(result.available).isTruthy();
});
Одна из ключевых проблем асинхронной валидации — устаревшие ответы.
Сценарий:
alex;alexander;Vest помогает управлять подобными ситуациями благодаря stateful suites.
Кэширование уменьшает количество запросов.
const cache = new Map();
async function checkEmailCached(email) {
if (cache.has(email)) {
return cache.get(email);
}
const result = await checkEmail(email);
cache.set(email, result);
return result;
}
Использование:
test('email', async () => {
const result = await checkEmailCached(data.email);
enforce(result.available).isTruthy();
});
Не каждая проблема должна быть ошибкой.
Пример:
import { warn } from 'vest';
test('password', 'Слишком простой пароль', async () => {
warn();
const result = await analyzePassword(data.password);
enforce(result.strong).isTruthy();
});
Теперь сообщение будет warning, а не error.
Группы позволяют логически объединять проверки.
import { group } from 'vest';
group('account', () => {
test('email', async () => {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
});
test('username', async () => {
const result = await checkUsername(data.username);
enforce(result.available).isTruthy();
});
});
Получение ошибок группы:
result.getErrorsByGroup('account');
Иногда поле необязательно.
import { optional } from 'vest';
optional('promoCode');
test('promoCode', async () => {
const result = await validatePromo(data.promoCode);
enforce(result.valid).isTruthy();
});
Если поле пустое, запрос не выполняется.
При больших формах нет смысла валидировать всё.
suite(data, 'email');
Теперь выполняются только тесты поля email.
Это критически важно для:
Проверки могут зависеть от данных формы.
const suite = vest.create((data) => {
if (data.accountType === 'business') {
test('taxId', async () => {
const result = await verifyTaxId(data.taxId);
enforce(result.valid).isTruthy();
});
}
});
Ошибка API не всегда означает ошибку поля.
Плохой подход:
test('email', async () => {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
});
Если сервер недоступен — возникнет unhandled rejection.
Корректный вариант:
test('email', 'Ошибка проверки email', async () => {
try {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
} catch (e) {
enforce.fail();
}
});
Полезно различать:
test('email', async () => {
try {
const result = await checkEmail(data.email);
if (result.error === 'RATE_LIMIT') {
throw new Error('Too many requests');
}
enforce(result.available).isTruthy();
} catch (e) {
console.error(e);
}
});
Современная асинхронная валидация часто требует отмены предыдущих запросов.
let controller;
async function validateEmail(email) {
if (controller) {
controller.abort();
}
controller = new AbortController();
const response = await fetch('/api/check-email', {
method: 'POST',
signal: controller.signal,
body: JSON.stringify({ email })
});
return response.json();
}
Использование:
test('email', async () => {
const result = await validateEmail(data.email);
enforce(result.available).isTruthy();
});
Пример проверки изображения.
test('avatar', 'Файл слишком большой', async () => {
const metadata = await getImageMetadata(data.avatar);
enforce(metadata.size).lessThan(5 * 1024 * 1024);
});
Проверка размеров изображения:
test('avatar', 'Недопустимое разрешение', async () => {
const metadata = await getImageMetadata(data.avatar);
enforce(metadata.width).greaterThan(300);
enforce(metadata.height).greaterThan(300);
});
import vest, {
test,
enforce,
skipWhen,
optional,
group
} from 'vest';
const suite = vest.create((data) => {
group('account', () => {
test('email', 'Некорректный email', () => {
enforce(data.email).matches(/^\S+@\S+\.\S+$/);
});
skipWhen(
res => res.hasErrors('email'),
() => {
test('email', 'Email уже используется', async () => {
const result = await checkEmail(data.email);
enforce(result.available).isTruthy();
});
}
);
test('username', 'Минимум 4 символа', () => {
enforce(data.username).longerThanOrEquals(4);
});
skipWhen(
res => res.hasErrors('username'),
() => {
test('username', 'Username занят', async () => {
const result = await checkUsername(data.username);
enforce(result.available).isTruthy();
});
}
);
});
test('password', 'Минимум 8 символов', () => {
enforce(data.password).longerThanOrEquals(8);
});
test('password', 'Пароль слишком простой', async () => {
const result = await analyzePassword(data.password);
enforce(result.score).greaterThan(60);
});
optional('promoCode');
test('promoCode', 'Промокод недействителен', async () => {
const result = await validatePromo(data.promoCode);
enforce(result.valid).isTruthy();
});
});
Хорошая архитектура обычно разделяет:
Отображение ошибок и loading-state.
Логика Vest suites.
HTTP-клиенты и API.
Кэширование, memoization, retry.
Используются:
Сначала:
enforce(data.email).matches(...)
Только потом:
await checkEmail(...)
suite(data, 'email');
Вместо:
suite(data);
Плохо:
test('field', async () => {
await hugeOperation();
});
Лучше:
Плохо:
test('email', async () => {
await checkEmail(data.email);
});
Лучше:
skipWhen(
res => res.hasErrors('email'),
() => {
test('email', async () => {
await checkEmail(data.email);
});
}
);
Без isPending() интерфейс может:
Без AbortController возникают:
Validation:
Email уже существует
Network:
Сервер недоступен
Это разные категории проблем.
Асинхронная валидация особенно востребована в React-приложениях.
Пример:
const result = suite(formData);
const emailPending = result.isPending('email');
const emailErrors = result.getErrors('email');
Использование в UI:
{emailPending && <Spinner />}
{emailErrors.map(error => (
<div key={error}>{error}</div>
))}
Vest часто используется вместе с:
Асинхронная валидация обычно подключается как внешний validation engine.
При росте приложения полезно:
Пример структуры:
validation/
├── suites/
├── async/
├── services/
├── cache/
├── validators/
└── utils/
export async function validateEmailAvailability(email) {
const result = await checkEmail(email);
enforce(result.available).isTruthy();
}
Использование:
test('email', async () => {
await validateEmailAvailability(data.email);
});
Наиболее эффективная стратегия:
Такая схема обеспечивает: