Vue предоставляет реактивную модель данных и механизм композиции логики через Composition API, что делает его удобной средой для интеграции схем валидации, построенных на чистых функциях. Vest хорошо ложится на этот подход благодаря декларативному описанию правил и отсутствию жёсткой привязки к UI-слою.
Ключевая идея интеграции заключается в разделении трёх уровней:
Такое разделение позволяет избегать смешивания логики ввода и бизнес-валидации.
Наиболее прямой способ подключения Vest в Vue — использование
computed и watch для запуска проверок при
изменении состояния формы.
import { reactive, computed, watch } from 'vue';
import { create, test, enforce } from 'vest';
const validationSuite = create((data) => {
test('username', 'Имя обязательно', () => {
enforce(data.username).isNotEmpty();
});
test('password', 'Пароль слишком короткий', () => {
enforce(data.password).longerThan(5);
});
});
export function useLoginForm() {
const form = reactive({
username: '',
password: ''
});
const result = computed(() => validationSuite(form));
const errors = computed(() => result.value.getErrors());
watch(form, () => {
validationSuite(form);
}, { deep: true });
return {
form,
errors
};
}
В этом подходе Vue отвечает за реактивность, а Vest — за вычисление состояния валидации.
При интеграции важно разделять:
Пример структуры:
const state = reactive({
form: {
email: '',
password: ''
},
validation: null
});
Инициализация валидации через watcher:
watch(
() => state.form,
(newValue) => {
state.validation = validationSuite(newValue);
},
{ deep: true, immediate: true }
);
Такой подход упрощает синхронизацию состояния без лишних вычислений в шаблоне.
Vue позволяет избегать лишних вызовов через computed, но
в случае Vest важно понимать: suite может быть тяжёлой операцией при
сложных формах.
Оптимизированный вариант:
const validation = computed(() => {
return validationSuite(form);
});
При таком подходе пересчёт происходит только при изменении зависимостей.
Каждое поле формы может получать ошибки через вспомогательные функции.
function getFieldErrors(result, field) {
return result.getErrors(field) || [];
}
Использование в компоненте:
const emailErrors = computed(() =>
getFieldErrors(validation.value, 'email')
);
Шаблон Vue:
<input v-model="form.email" />
<div v-if="emailErrors.length">
<p v-for="err in emailErrors" :key="err">
{{ err }}
</p>
</div>
Vest поддерживает асинхронные проверки, что критично при интеграции с API.
test('email', 'Email уже занят', async () => {
const response = await fetch(`/api/check-email?value=${data.email}`);
const result = await response.json();
enforce(result.available).isTruthy();
});
Во Vue важно учитывать состояние загрузки:
const state = reactive({
loading: false
});
watch(
() => form.email,
async () => {
state.loading = true;
await validationSuite(form);
state.loading = false;
}
);
Для масштабируемых приложений логично вынести логику в отдельный composable.
import { reactive, watch } from 'vue';
export function useValidation(suite, form) {
const state = reactive({
result: null,
errors: {}
});
watch(
form,
() => {
state.result = suite(form);
state.errors = state.result.getErrors();
},
{ deep: true, immediate: true }
);
return state;
}
Использование:
const { result, errors } = useValidation(validationSuite, form);
Vest позволяет группировать проверки, что удобно для сложных форм.
test('password', 'Пароли не совпадают', () => {
enforce(data.password)
.equals(data.confirmPassword);
});
Во Vue это часто связывается с computed-логикой:
const passwordsMatch = computed(() =>
form.password === form.confirmPassword
);
Однако предпочтительнее держать правило в Vest, чтобы не дублировать логику.
При больших формах важно учитывать:
Использование watch на конкретные поля:
watch(() => form.email, () => {
validationSuite(form);
});
import { debounce } from 'lodash-es';
const runValidation = debounce(() => {
validationSuite(form);
}, 300);
watch(form, runValidation, { deep: true });
Vest уже кэширует результаты внутри suite, что снижает нагрузку при повторных вызовах.
При использовании кастомных компонентов Vue:
<BaseInput v-model="form.username" />
Ошибки передаются через props:
<BaseInput
v-model="form.username"
:errors="getFieldErrors(validation, 'username')"
/>
Компонент:
export default {
props: {
modelValue: String,
errors: Array
}
};
Vest не управляет UI-состоянием взаимодействия, поэтому это реализуется во Vue:
const state = reactive({
touched: {}
});
function markTouched(field) {
state.touched[field] = true;
}
Использование:
<input
v-model="form.email"
@blur="markTouched('email')"
/>
Отображение ошибок:
const showError = (field) =>
state.touched[field] && errors[field]?.length;
При использовании TypeScript в Vue можно типизировать форму и suite:
type Form = {
email: string;
password: string;
};
const form = reactive<Form>({
email: '',
password: ''
});
И типизированная валидация:
const suite = create((data: Form) => {
test('email', () => {
enforce(data.email).isNotEmpty();
});
});
Типичная организация интеграции:
/validation
loginSuite.js
registerSuite.js
/composables
useValidation.js
useLoginForm.js
/components
BaseInput.vue
LoginForm.vue
Такой подход снижает связанность между UI и правилами проверки и упрощает повторное использование логики валидации между компонентами Vue.