- Home »

Создание пользовательской валидации в Angular
Каждый фронтенд-разработчик рано или поздно сталкивается с необходимостью создать валидацию, которая выходит за рамки стандартных решений Angular. Встроенные валидаторы хороши для базовых проверок, но когда нужно проверить уникальность email’а на сервере, валидировать сложные бизнес-правила или создать специфичную логику для конкретного проекта — тут без пользовательских валидаторов не обойтись. Особенно это актуально для серверных приложений, где часто требуется интеграция с различными API и сложная логика обработки форм.
Как работает пользовательская валидация в Angular
Валидатор в Angular — это простая функция, которая принимает AbstractControl и возвращает либо null (если валидация прошла успешно), либо объект с описанием ошибки. Звучит просто, но дьявол кроется в деталях.
Существует два типа пользовательских валидаторов:
- Синхронные валидаторы — выполняются мгновенно и возвращают результат сразу
- Асинхронные валидаторы — возвращают Promise или Observable, идеально подходят для проверки данных на сервере
Базовая структура синхронного валидатора:
export function customValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value || someValidationLogic(value)) {
return null; // валидация прошла успешно
}
return { 'customError': { value: value } }; // ошибка валидации
}
Пошаговая настройка пользовательской валидации
Начнем с простого примера — создадим валидатор для проверки силы пароля:
// validators/password-strength.validator.ts
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) {
return null; // не валидируем пустые значения
}
const hasNumber = /[0-9]/.test(value);
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const minLength = value.length >= 8;
const passwordValid = hasNumber && hasUpperCase && hasLowerCase && hasSpecialChar && minLength;
if (!passwordValid) {
return {
'passwordStrength': {
hasNumber: hasNumber,
hasUpperCase: hasUpperCase,
hasLowerCase: hasLowerCase,
hasSpecialChar: hasSpecialChar,
minLength: minLength
}
};
}
return null;
}
Теперь подключим этот валидатор к форме:
// app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { passwordStrengthValidator } from './validators/password-strength.validator';
@Component({
selector: 'app-root',
template: `
`
})
export class AppComponent {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
password: ['', [Validators.required, passwordStrengthValidator]]
});
}
}
Асинхронная валидация: проверка на сервере
Для серверных приложений часто нужна проверка уникальности данных. Вот пример асинхронного валидатора для проверки email’а:
// validators/email-unique.validator.ts
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class EmailUniqueValidator implements AsyncValidator {
constructor(private http: HttpClient) {}
validate(control: AbstractControl): Observable
if (!control.value) {
return of(null);
}
return of(control.value).pipe(
debounceTime(300), // задержка для уменьшения количества запросов
switchMap(email =>
this.http.get<{exists: boolean}>(`/api/check-email?email=${email}`)
),
map(response => response.exists ? { 'emailTaken': true } : null),
catchError(() => of(null)) // игнорируем ошибки сети
);
}
}
Использование асинхронного валидатора:
// app.component.ts
export class AppComponent {
userForm: FormGroup;
constructor(
private fb: FormBuilder,
private emailValidator: EmailUniqueValidator
) {
this.userForm = this.fb.group({
email: ['',
[Validators.required, Validators.email], // синхронные валидаторы
[this.emailValidator.validate.bind(this.emailValidator)] // асинхронный валидатор
]
});
}
}
Практические примеры и кейсы
Рассмотрим несколько полезных валидаторов для реальных проектов:
1. Валидатор для проверки доменного имени
export function domainValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
if (!domainRegex.test(value)) {
return { 'invalidDomain': true };
}
// Проверка длины доменного имени
if (value.length > 253) {
return { 'domainTooLong': true };
}
return null;
}
2. Валидатор для проверки IP-адреса
export function ipAddressValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (!ipv4Regex.test(value) && !ipv6Regex.test(value)) {
return { 'invalidIp': true };
}
return null;
}
3. Валидатор для проверки порта
export function portValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const port = parseInt(value, 10);
if (isNaN(port) || port < 1 || port > 65535) {
return { 'invalidPort': true };
}
// Проверка на зарезервированные порты
const reservedPorts = [22, 23, 25, 53, 80, 110, 143, 443, 993, 995];
if (reservedPorts.includes(port)) {
return { 'reservedPort': true };
}
return null;
}
Сравнение подходов к валидации
Подход | Производительность | Сложность реализации | Переиспользование | Тестирование |
---|---|---|---|---|
Встроенные валидаторы | Отличная | Низкая | Высокое | Простое |
Пользовательские синхронные | Хорошая | Средняя | Высокое | Средней сложности |
Пользовательские асинхронные | Зависит от сервера | Высокая | Высокое | Сложное |
Директивы валидации | Хорошая | Высокая | Среднее | Сложное |
Продвинутые техники и оптимизация
Для высоконагруженных приложений важно оптимизировать валидацию. Вот несколько полезных техник:
1. Кеширование результатов асинхронной валидации
@Injectable({
providedIn: 'root'
})
export class CachedEmailValidator implements AsyncValidator {
private cache = new Map
constructor(private http: HttpClient) {}
validate(control: AbstractControl): Observable
const email = control.value;
if (!email) return of(null);
// Проверяем кеш
if (this.cache.has(email)) {
const exists = this.cache.get(email);
return of(exists ? { 'emailTaken': true } : null);
}
return this.http.get<{exists: boolean}>(`/api/check-email?email=${email}`).pipe(
map(response => {
this.cache.set(email, response.exists); // сохраняем в кеш
return response.exists ? { 'emailTaken': true } : null;
}),
catchError(() => of(null))
);
}
}
2. Композиция валидаторов
export function createServerConfigValidator() {
return function(control: AbstractControl): ValidationErrors | null {
const errors: ValidationErrors = {};
// Проверяем каждое поле конфигурации
const domainError = domainValidator(control.get('domain')!);
const ipError = ipAddressValidator(control.get('ip')!);
const portError = portValidator(control.get('port')!);
if (domainError) errors['domain'] = domainError;
if (ipError) errors['ip'] = ipError;
if (portError) errors['port'] = portError;
return Object.keys(errors).length ? errors : null;
};
}
Интеграция с другими инструментами
Пользовательские валидаторы отлично интегрируются с популярными библиотеками:
С библиотекой Yup
import * as yup from 'yup';
export function yupValidator(schema: yup.Schema
return function(control: AbstractControl): ValidationErrors | null {
try {
schema.validateSync(control.value);
return null;
} catch (error) {
return { 'yupError': error.message };
}
};
}
// Использование
const serverSchema = yup.object().shape({
hostname: yup.string().required().min(3).max(253),
port: yup.number().required().min(1).max(65535),
ssl: yup.boolean().required()
});
this.serverForm = this.fb.group({
config: ['', yupValidator(serverSchema)]
});
С RxJS операторами
export function debounceValidator(validator: AsyncValidatorFn, delay: number = 300): AsyncValidatorFn {
return function(control: AbstractControl): Observable
return timer(delay).pipe(
switchMap(() => validator(control))
);
};
}
Автоматизация и скрипты
Для автоматизации создания валидаторов можно использовать Angular CLI schematics:
// Создание схемы для генерации валидаторов
ng generate schematic validator --name=custom-validator
// Генерация валидатора из схемы
ng generate validator:custom-validator --name=domain-validator
Также полезно создать скрипт для автоматического тестирования валидаторов:
// test-validators.spec.ts
describe('Custom Validators', () => {
const testCases = [
{ validator: domainValidator, valid: ['example.com', 'sub.example.com'], invalid: ['', 'invalid..domain'] },
{ validator: portValidator, valid: [8080, 3000], invalid: [0, 70000, 'abc'] }
];
testCases.forEach(({ validator, valid, invalid }) => {
valid.forEach(value => {
it(`should validate ${value} as valid`, () => {
expect(validator({ value } as AbstractControl)).toBeNull();
});
});
invalid.forEach(value => {
it(`should validate ${value} as invalid`, () => {
expect(validator({ value } as AbstractControl)).toBeTruthy();
});
});
});
});
Производительность и мониторинг
Для мониторинга производительности валидаторов можно использовать декораторы:
function measureValidation(name: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`Validator ${name} took ${end - start} milliseconds`);
return result;
};
};
}
// Использование
@measureValidation('passwordStrength')
export function passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
// логика валидации
}
Интересные факты и статистика
Согласно исследованиям Google, формы с хорошей валидацией увеличивают конверсию на 25-40%. При этом:
- Пользователи покидают форму в 67% случаев, если валидация работает медленно
- Асинхронная валидация должна срабатывать не позднее 300ms для комфортного UX
- Кеширование результатов валидации может снизить нагрузку на сервер до 80%
Для развертывания Angular-приложений с продвинутой валидацией рекомендую использовать VPS-серверы с предустановленным Node.js, а для высоконагруженных проектов — выделенные серверы с возможностью тонкой настройки.
Альтернативные решения
Стоит также рассмотреть альтернативные подходы к валидации:
- Formly — динамическое создание форм с встроенной валидацией
- Reactive Forms — мощный инструмент для создания сложных форм
- Joi — библиотека для валидации данных, популярная в Node.js
- Vest — легковесная альтернатива для создания валидационных наборов
Полезные ресурсы:
- Официальная документация Angular по валидации
- Исходный код Angular Forms
- RxJS операторы для асинхронной валидации
Заключение и рекомендации
Пользовательская валидация в Angular — это мощный инструмент, который позволяет создавать гибкие и надежные формы для любых задач. Основные рекомендации:
- Используйте синхронные валидаторы для простых проверок — они быстрее и не создают лишней нагрузки
- Применяйте асинхронную валидацию только когда необходимо обращение к серверу
- Кешируйте результаты асинхронных валидаторов для улучшения производительности
- Используйте debouncing для уменьшения количества запросов к серверу
- Тестируйте валидаторы отдельно от компонентов — это упрощает отладку
- Создавайте переиспользуемые валидаторы для часто встречающихся случаев
Для серверных приложений особенно важно правильно настроить валидацию на уровне API и фронтенда. Это поможет избежать проблем с безопасностью и обеспечить хорошую производительность. Помните: хорошая валидация — это не только техническая задача, но и важная часть пользовательского опыта.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.