Зачем нам Node.js или Angular на бэкенде
- пятница, 8 ноября 2024 г. в 00:00:03
Всем привет! Меня зовут Александр, я разрабатываю low-code платформу Eftech.Factory в компании Effective Technologies. В этой статье я хочу поделиться тем, как и почему в стеке нашего продукта появился Node.js. Рассмотрим одно из основных преимуществ Node.js (внезапно это JavaScript) и то, как он помогает нам сэкономить время в два раза на разработку и сопровождение.
Из-за названия статьи может возникнуть путаница: чаще всего, когда речь идет об Angular на бэкенде, подразумевается Server Side Rendering (SSR). Однако в данной статье мы не будем обсуждать SSR, а сосредоточимся на переиспользовании кода и использовании Angular на бэкенде. Давайте начнем!
Какие бы классные и интересные приложения мы ни создавали, все они требуют получения данных — от пользователей, других систем или даже датчиков автоматизации. Важным аспектом этого процесса является корректность получаемых данных, так как она критически важна для их обработки и служит залогом успешного результата работы программы.
Всё начинается с задачи обработки данных. Команда аналитиков формулирует требования к данным, в том числе и требования к проверке этих данных. На самом деле, мы сталкиваемся с двумя задачами: задача для фронтенда, чтобы наш интерфейс был отзывчивым и удобным, и задача для бэкенда, чтобы наше приложение было безопасным и надежным. Не стоит спрашивать, почему нельзя ограничиться проверками только на фронтенде — надеюсь, вы так не делаете! =)
Чаще всего эти задачи передаются бэкенд и фронтенд разработчикам, которые реализуют одну и ту же логику. И вот тут мы подходим к главному аргументу в пользу использования Node.js: проверки на фронтенде мы реализуем на TypeScript, так почему бы не использовать те же проверки и на бэкенде? Преимущества очевидны:
1. Экономия времени и ресурсов — нам больше не нужно писать два раза одну и ту же валидацию;
2. Отсутствие рассинхрона валидации — когда в требования вносятся изменения, а ресурса одного из направлений недостаточно, мы можем столкнуться с задержками. Если не уследить за процессом, это может привести к возникновению багов;
3. Единое поведение реализации — например, в разных языках могут быть по-разному реализованы работа с числами: округление, переполнение и другие нюансы.
Таким образом, использование единого подхода к валидации на фронтенде и бэкенде не только упрощает процесс разработки, но и делает его более надёжным.
Вижу цель — иду к ней! На фронтенде мы используем Angular, а на бэкенде разнообразие сервисов из PHP и Golang. Как же нам реализовать единую валидацию? Ответ на поверхности - переписать PHP сервисы на Node.js и использовать код с фронтенда. Однако всё не так просто. Чтобы переиспользовать код с фронтенда, необходимо соблюсти несколько условий в реализации:
Angular — тут нужно отметить, что архитектура Angular является одной из причин, по которой мы его выбрали. У Angular понятные модули, готовая система внедрения зависимостей (DI) и достаточно мощные реактивные формы, которые можно переиспользовать независимо от DOM и API браузера. Именно это мы и сделали.
Структура данных — нам нужно единое описание структуры данных и правил ее проверки. Это может быть описание цепочки валидаторов для полей. В нашем случае, в low-code на базе JSON Schema, мы описываем элементы UI, в которых есть блок validation с правилами проверок, реализованных с помощью небольших функций, которые мы называем helper. Почему мы не валидируем по JSON Schema? Дело в том, что не всегда структура данных соответствует UI, а сами проверки могут быть весьма сложными.
Тем не менее JSON Schema служит для проверок самого low-code, подсказок, документации и интерфейса Eftech.Studio. Ой, что-то я отвлекся — об этом в следующий раз =)
Логика проверок также должна обладать определенным уровнем абстракций и не иметь прямых зависимостей от того или иного окружения. Все зависимости от API браузера и Node.js мы выносим в слой сервисов и передаем через dependency injection (DI - внедрение зависимостей), Таким образом, с помощью DI мы можем переопределять поведение сервиса или целого валидатора. Когда это может понадобиться? Например, если мы хотим проверить существование записи: на фронтенде мы делаем HTTP-запрос, а на бэкенде — запрос к базе данных. Для этого мы выносим FindObject как зависимость c соответствующими реализациям на фронтенде и бэкенде для ExistValidator.
Нам понадобится единое описание проверок данных. В нашем случае это описание хранится в low-code:
{
"@title": "Добавление пользователя",
"type": "object",
"properties": {
"email": {
"type": "string",
"validation": [
{
"rule": {
"$match": {
"pattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{1,1000}$"
}
},
"message": "Некорректный адрес электронной почты",
"events": [
"onUpdate",
"onBackend"
]
},
{
"rule": {
"$api-data": {
"query": "user/email_exists"
}
},
"message": "Пользователь с таким адресом электронной почты уже зарегистрирован в системе",
"events": [
"onBackend"
]
}
]
}
}
}
В этом примере описано две проверки на соответствие паттерну email и существование пользователя в сервисе данных. Важно отметить, что первая проверка будет выполнена как на фронтенде, так и на бэкенде, тогда как вторая проверка сработает только на бекенде, чтобы избежать излишней нагрузки. Ключевые слова, начинающиеся с $, представляют собой названия вспомогательных функций (хелперов). В общем случае это могут быть названия валидаторов, а описание может находиться в коде общего слоя.
Если реализацию проверки хелпером $match мы можем переиспользовать без каких либо изменений за счет использования String.match(), то с хелпером $api-data все немного сложнее — в обоих случаях и с фронтенда, и с бэкенда отправляется HTTP-запрос в сервис данных, но на фронтенде у нас реализация запроса на HttpClient от Angular, а на бэкенде undici.
Реализация самого хелпера не будет отличаться на фронтенде и бэкенде, чтобы его использовать мы помечаем класс для dependency injector Angular с помощью декоратора Injectable. На бэкенде мы используем NestJS, соответственно для бэкенда мы также помечаем класс для DI с помощью декоратора InjectableGlobal, который мы позаимствовали из NestJS для исключения коллизии в именовании.
@Injectable({
providedIn: 'root',
})
@InjectableGlobal()
export class ApiDataHelper<T> implements Helper {
constructor(protected http: HttpClient) {}
apply(options: unknown, context: unknown): Observable<T> {}
}
Важно отметить, что использование NestJS очень упрощает задачу по работе с зависимостями. Ранее мы использовали реализацию ReflectiveInjector из Angular в сервисах на Node.js, однако начиная с 16-й версии ее удалили.
Теперь добавим на бэкенде класс, совместимый по API с HttpClient
Injectable()
@InjectableGlobal()
export class HttpService<T> {
public get(url, options?: HttpOptions): Observable<T> {}
public post(url, body?: unknown, options: HttpOptions = {}): Observable<T> {}
}
Останется только переопределить соответствующий класс в модуле NestJS на бэкенде:
@Module({
providers: [
{
provide: BaseHttpService,
useValue: HttpService,
},
],
})
export class ApiModule {}
Поздравляю! У нас готова реализация, которую осталось только применить. Здесь нам на помощь придут реактивные формы Angular. Результаты работы хелперов обернуты в RxJS потоки, что позволяет использовать их без каких-либо изменений в asyncValidators контролов Angular:
control.setAsyncValidators(validators)
На бэкенде также необходимо создать контролы, заполнить их значениями и проверить результат валидации:
validate(
schema: Schema,
data: unknown,
path = '/',
context: ApplyContext
): Observable<ValidateResult> {
// Cоздаем форму из контролов ангуляр
let form = this.schemaService.create(schema, context);
// Заполняем данные из POST
form.reset(data);
return form.statusChanges.pipe(
startWith(form.status),
filter((status) => status !== 'PENDING'),
map(() => {
// Формируем ответ с ошибками, если они есть
const errors = this.buildErrors(form);
return this.buildResponse(form, data, errors);
})
);
}
Стоит отметить, что schemaService имеет единую реализацию как для фронтенда, так и для бэкенда по построению реактивной формы и установке валидации. В случае успешной проверки сервис выполняет целевой запрос на сохранение данных; если валидация не прошла, то в ответе возвращается список ошибок, которые отобразятся на форме пользователя для исправления.
Angular упрощает создание реактивных форм и клиентскую валидацию, благодаря чему пользователи сразу видят сообщения об ошибках. Это, в свою очередь, делает взаимодействие с приложением более удобным и интуитивно понятным. На сервере валидация также помогает нам контролировать целостность данных, исключая дублирование кода и экономя время.
Используя Node.js на бэкенде и Angular на фронтенде, мы минимизируем повторяющийся код, ускоряем разработку и добиваемся согласованности между клиентской и серверной частями приложения. Такой подход не только упрощает разработку, но и делает приложение более надежным и легким в поддержке.