Пишешь ли кастомную валидацию
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Мой подход к кастомной валидации в веб-разработке
Да, я регулярно пишу кастомную валидацию в своих проектах, поскольку встроенных механизмов HTML5 и стандартных валидаторов часто недостаточно для сложных бизнес-правил. Кастомная валидация позволяет создавать более точные, пользовательские и контекстно-зависимые проверки данных.
Когда я применяю кастомную валидацию
Я использую кастомные решения в следующих случаях:
- Сложные бизнес-правила, которые невозможно выразить через стандартные атрибуты вроде
patternилиrequired - Зависимые поля, когда валидность одного поля зависит от значения другого
- Асинхронные проверки, например, проверка уникальности email или username через API
- Кастомные сообщения об ошибках с локализацией и специфичной для приложения логикой
- Валидация динамических форм, где структура формы меняется в зависимости от действий пользователя
Примеры реализации
1. Синхронная валидация с использованием Constraint Validation API
class CustomValidator {
static validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('Пароль должен содержать минимум 8 символов');
}
if (!/[A-Z]/.test(password)) {
errors.push('Пароль должен содержать хотя бы одну заглавную букву');
}
if (!/\d/.test(password)) {
errors.push('Пароль должен содержать хотя бы одну цифру');
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push('Пароль должен содержать хотя бы один специальный символ');
}
return {
isValid: errors.length === 0,
errors
};
}
static validatePhone(phone) {
// Российские номера в различных форматах
const phoneRegex = /^(\+7|8)[\s\-]?\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2}$/;
return phoneRegex.test(phone);
}
}
// Использование в React компоненте
const PasswordField = () => {
const [errors, setErrors] = useState([]);
const handlePasswordChange = (e) => {
const validation = CustomValidator.validatePassword(e.target.value);
setErrors(validation.errors);
// Установка кастомной валидности для поля
e.target.setCustomValidity(
validation.isValid ? '' : validation.errors.join('. ')
);
};
return (
<div>
<input
type="password"
onChange={handlePasswordChange}
required
/>
{errors.map((error, index) => (
<div key={index} className="error">{error}</div>
))}
</div>
);
};
2. Асинхронная валидация с debounce
import { debounce } from 'lodash';
const AsyncValidator = {
checkEmailAvailability: debounce(async (email) => {
if (!email || email.length < 3) {
return { available: false, message: 'Email слишком короткий' };
}
try {
const response = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Ошибка проверки email:', error);
return { available: false, message: 'Ошибка сервера' };
}
}, 500),
validateAsyncForm: async (formData) => {
const validations = await Promise.all([
this.checkEmailAvailability(formData.email),
// другие асинхронные проверки
]);
return validations.every(v => v.available);
}
};
3. Валидация с использованием библиотек и собственных решений
Я часто комбинирую библиотечные решения с кастомной логикой:
// Пример с Yup и кастомными правилами
import * as yup from 'yup';
const customValidationSchema = yup.object().shape({
username: yup
.string()
.required('Имя пользователя обязательно')
.test(
'no-spaces',
'Имя пользователя не должно содержать пробелы',
value => !/\s/.test(value)
)
.test(
'custom-rule',
'Имя пользователя должно содержать хотя бы одну цифру',
value => /\d/.test(value)
),
birthDate: yup
.date()
.required()
.test(
'age-restriction',
'Пользователь должен быть старше 18 лет',
function(value) {
const today = new Date();
const birthDate = new Date(value);
const age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
return age > 18 || (age === 18 && monthDiff >= 0);
}
),
confirmPassword: yup
.string()
.oneOf([yup.ref('password'), null], 'Пароли должны совпадать')
.required('Подтверждение пароля обязательно')
});
// Кастомная валидация для сложных условий
const validateBusinessRules = (values) => {
const errors = {};
// Пример: проверка зависимости полей
if (values.subscriptionType === 'premium' && !values.creditCard) {
errors.creditCard = 'Для премиум подписки требуется кредитная карта';
}
// Пример: кросс-полевая валидация
if (values.startDate && values.endDate) {
const start = new Date(values.startDate);
const end = new Date(values.endDate);
if (end <= start) {
errors.endDate = 'Дата окончания должна быть позже даты начала';
}
const diffTime = Math.abs(end - start);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 365) {
errors.endDate = 'Период не может превышать 1 год';
}
}
return errors;
};
Лучшие практики, которые я соблюдаю
-
Многоуровневая валидация:
- Клиентская валидация для мгновенного feedback
- Серверная валидация для безопасности
- Валидация на уровне бизнес-логики
-
Юзабилити аспекты:
- Валидация на лету (onChange) для сложных полей
- Валидация при потере фокуса (onBlur) для простых полей
- Отложенная валидация с debounce для асинхронных проверок
- Показ ошибок в контексте поля и сводной форме
-
Производительность:
- Мемоизация функций валидации
- Отмена pending асинхронных запросов
- Ленивая валидация для невидимых полей
-
Доступность (a11y):
- Связь ошибок с полями через
aria-describedby - Правильные роли и алерты для скринридеров
- Фокус на первую ошибку после сабмита
- Связь ошибок с полями через
Архитектурные подходы
Я предпочитаю создавать реиспользуемые валидаторы, которые можно тестировать изолированно. Часто выношу валидационную логику в отдельные сервисы или хук-функции:
// Кастомный хук для валидации в React
const useValidation = (schema, initialValues) => {
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = useCallback(async (name, value) => {
try {
await schema.validateAt(name, { [name]: value });
setErrors(prev => ({ ...prev, [name]: '' }));
return true;
} catch (error) {
setErrors(prev => ({ ...prev, [name]: error.message }));
return false;
}
}, [schema]);
const validateForm = useCallback(async (values) => {
try {
await schema.validate(values, { abortEarly: false });
setErrors({});
return { isValid: true, errors: {} };
} catch (validationErrors) {
const formattedErrors = {};
validationErrors.inner.forEach(error => {
formattedErrors[error.path] = error.message;
});
setErrors(formattedErrors);
return { isValid: false, errors: formattedErrors };
}
}, [schema]);
return {
errors,
touched,
setTouched,
validateField,
validateForm,
hasErrors: Object.values(errors).some(error => error)
};
};
Кастомная валидация — это не просто проверка корректности данных, а важная часть пользовательского опыта и безопасности приложения. Я всегда стремлюсь найти баланс между строгостью проверок и удобством для пользователя, создавая валидацию, которая направляет пользователя к успешному заполнению формы, а не просто блокирует его ошибками.