| Воскресенье, 28 июля, 2019
Метки: Angular, TypeScript Комментарии: 0
Иногда случается так, что реализация простейших фич заканчивается еще большими сложностями. В попытках упростить код мы начинаем импровизировать и усложняем код еще больше. И в итоге мы приходим к какой-то очень странной и корявой архитектуре, с которой никто не хочет иметь дело. В Angular-приложениях, в таких ситуациях на помощь приходит Ngrx/store - библиотека, которая и позволяет предотвращать и снижать риск появления этой запутанности, возникающей по мере реализации новых бизнес-требований и новых фич.
Функциональное программирование, которое активно применяется в ngrx/store, накладывает некоторые ограничения в реализации нужных нам функций. Но благодаря этому, мы достигаем более адекватного состояния кода снаружи в среде их использования. Речь идет, о так называемых, чистых функциях (pure functions). В ngrx/store редьюсеры, селекторы и rxjs-операторы являются именно такими чистыми функциями.
С чистыми функциями работать намного проще и приятнее, ведь их анализ, тестирование, отладка, комбинирование и параллельное использование осуществляется гораздо проще и безопаснее.
Вспомним, функция считается чистой при следующих условиях:
Но в реальности совсем без побочки в приложении обойтись не получается, поэтому побочные эффекты в ngrx/store изолированы и работают отдельно, чтобы остальная часть приложение могла функционировать исключительно на чистых функциях.
Когда пользователь сабмитит форму с данными, приложения нужно отправить эти данные на сервер и получить ответ. Это можно реализовать в побочном эффекте. Например, реализуем это в компоненте:
this.store.dispatch({ type: "SAVE_DATA", payload: data }); this.saveData(data) // POST request to server .map(res => this.store.dispatch({type: "DATA_SAVED"})) .subscribe((...) => { ... });
Но было бы прекрасно, если бы мы обошлись в компоненте исключительно только вызовом экшена, а побочный эффект реализовали бы где-то в другом месте.
Для этого нам на помощь придет библиотека Ngrx/effects, которая как раз и реализует механизм побочных эффектов в ngrx/store. Она слушает отправляемые actions в rxjs-потоке, выполняет побочные действия, и возвращает новые немедленно исполняемые или асинхронные действия обратно, которые передаются прямо в редьюсер.
Таким образом, мы имеем возможность организовать более чистый код, используя реактивный подход для организации побочных эффектов. То есть, действие мы отправляем только в компоненте, а побочный эффект мы обрабатываем в отдельном классе.
@Effect() saveData$ = this.actions$ .ofType('SAVE_DATA') .pluck('payload') .switchMap(data => this.saveData(data)) .map(res => ({type: "DATA_SAVED"}))
Так мы упрощаем разработку компонентов. Только тем, что отправляем действия (dispatch actions) и подписываемся на изменения (subscribe to observables).
Ngrx/effects очень мощное решение, но в тоже время с ним очень легко и нагородить ненужного и запутанного кода. Рассмотрим несколько антипатернов, которые следует избегать и не усложнять себе разработку.
Предположим, вы создаете мультимедийное приложение, и в дереве состояний у вас следующие свойства:
export interface State { mediaPlaying: boolean; audioPlaying: boolean; videoPlaying: boolean; }
Так как аудио - это тоже мультимедиа, всякий раз, когда audioPlaying имеет значение true, у mediaPlaying должно быть аналогичное значение. Возникает вопрос: «Как синхронизировать это состояние, чтобы при обновлении audioPlaying обновлялся и mediaPlaying?»
Неправильно: использовать эффекты!
@Effect() playMediaWithAudio$ = this.actions$ .ofType("PLAY_AUDIO") .map(() => ({type: "PLAY_MEDIA"}))
Правильно: если состояние mediaPlaying полностью предсказывается другой частью дерева состояний, то оно не является истинным. Это состояние не истинное, а производное. Оно предназначено не для хранилища, а для селектора.
audioPlaying$ = this.store.select('audioPlaying'); videoPlaying$ = this.store.select('videoPlaying'); mediaPlaying$ = Observable.combineLatest( this.audioPlaying$, this.videoPlaying$, (audioPlaying, videoPlaying) => audioPlaying || videoPlaying )
Теперь мы нормализовали и очистили состояние, и не нужно использовать ngrx/effects для того, что не является побочным эффектом.
Представьте, что у вас такое дерево состояний:
export interface State { items: {[index: number]: Item}; favoriteItems: number[]; }
Допустим, по бизнес-требованиям нужно иметь возможность удалять элементы из общего списка элементов (items) и из списка отобранных (favoriteItems). Для этого мы создаем два действия DELETE_ITEM_SUCCESS и REMOVE_FAVORITE_ITEM, которые удаляют элементы из соответствующих соcтояний. Но есть одна неувязка, при удалении элемента из общего списка нам нужно удалить его и из отобранных, если он там находится.
Неправильно: использовать эффекты!
@Effect() this.actions$ .ofType("DELETE_ITEM_SUCCESS") .map(() => ({type: "REMOVE_FAVORITE_ITEM_ID"}))
Итак, у нас получилось два действия, которые отправляются друг за другом и два редюсера, возвращающих новые состояния по очереди.
Правильно: DELETE_ITEM_SUCCESS может обрабатывать как редюсер элементов, так и редюсер FavoritesItems.
export function favoriteItemsReducer(state = initialState, action: Action) { switch(action.type) { case 'REMOVE_FAVORITE_ITEM': case 'DELETE_ITEM_SUCCESS': const itemId = action.payload; return state.filter(id => id !== itemId); default: return state; } }
Задача этих действий, разделить понятие "что случилось" от понятия "как должно изменится состояние". А это как раз и работа редюсеров вызвать соответствующие изменения состояний.
Copyright © CodeHint.ru 2013-2024 (v2.4.7 - работает на Angular Universal)Калькулятор инвест-портфеля