| Воскресенье, 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-2025 (v2.4.7 - работает на Angular Universal)Калькулятор инвест-портфеля