Angular: понятное введение в NGRX

image Цель этой статьи — дать чистое и ясное представление о ngrx. Для этого я объясню, что нужно знать и понимать о ngrx, а затем мы увидим это в действии с простыми и понятными примерами кода.

Вот список тем, которые мы будем обсуждать в этой статье:

  • Что такое ngrx
  • Преимущества использования ngrx
  • Недостатки использования ngrx
  • Когда использовать ngrx
  • Действия, Редукторы, Селекторы, Хранилище и Эффекты

Продолжение статьи с примером использования: «Angular: пример использования NGRX».

Что такое NGRX


NGRX — это группа библиотек, «вдохновленных» шаблоном Redux, который, в свою очередь, «вдохновлен» шаблоном Flux. Проще говоря, это означает, что шаблон Redux является упрощенной версией Flux шаблона, а NGRX — angular/rxjs версией Redux шаблона.

Что я имею в виду под «angular/rxjs» версией redux… «angular» часть заключается в том, что ngrx — это библиотека для использования в приложениях angular. Часть «rxjs» заключается в том, что реализация ngrx работает вокруг потока rxjs. Это означает, что он работает с использованием наблюдаемых и различных наблюдаемых операторов, предоставляемых «rxjs».

Основной целью этой схемы является обеспечение предсказуемого состояния контейнера, основанного на трех основных принципах


Давайте рассмотрим три принципа модели Redux и укажем на наиболее важные преимущества, которые они дают.

Единственный источник правды


В случае с архитектурой redux/ngrx это означает, что состояние всего вашего приложения хранится в дереве объектов в пределах одного хранилища.
В одном хранилище? О хранилищах мы поговорим позже, но для общего понимания, они несут ответственность за сохранение состояния и применение к нему изменений, когда им говорят об этом (когда отправляется действие, мы также поговорим о них позже).

Преимущества наличия одного источника правды многочисленны, но для меня наиболее интересным (потому что это то, что будет влиять на любое angular приложение) является следующее:

  • Когда вы создаете приложение Angular, обычно вы разделяете состояние и обрабатываете несколько сервисов. По мере того, как ваше приложение растет, отслеживать изменения вашего состояния, становится все сложнее и сложнее и в итоге оно становиться беспорядочным, его сложно отлаживать и поддерживать.Наличие единственного источника правды решает эту проблему, поскольку состояние обрабатывается только в одном объекте и в одном месте, поэтому отладка или добавление изменений становится намного проще.

Состояние read-only (только для чтения)


Вы никогда не измените состояние напрямую, вместо этого вы будете отправлять действия, которые описывают действия с объектом (это могут быть такие вещи, как получение, добавление, удаление или обновление состояния).
Отправить действие?.. Мы поговорим о действиях позже, но для общего понимания, это идентификаторы операции над вашим приложением, и они могут быть запущены (или отправлены), чтобы сообщить приложению выполнить операцию, которую представляет действие.
Избегая обновлять состояние из разных мест и имея централизованное место для внесения изменений, которое реагирует на конкретные действия, вы получаете много преимуществ. Не говоря уже о самых важных:

  • Вы знаете, что любое изменение в состоянии произойдет только в одном месте. Это оказывает большое влияние на отладку и тестирование.
  • Вы знаете, что если посылается определенное действие, то операция в состоянии всегда одна и та же. Опять же, это напрямую влияет на отладку и тестирование.

Изменения вносятся с простыми функциями


Операция, инициируемая отправкой действия, будет простой функцией, называемой в рамках архитектуры redux, редукторами.

Эти редукторы (помните, что они простые функции) получают действие и состояние, в зависимости от отправленного действия (обычно с оператором switch), они выполняют операцию и возвращают объект нового состояния.

Состояние в redux приложении является неизменным! Поэтому, когда редуктор что-то меняет в состоянии, он возвращает новый объект состояния.

Преимущества использования чистых функций хорошо известны, как, например, тот факт, что они могут быть немедленно протестированы, если вы передадите те же аргументы, которые вы получите в результате.

Этот подход также позволяет нам перемещаться между различными экземплярами нашего состояния с помощью инструментов разработки Redux/ngrx и видеть, что изменилось между экземплярами и кто его изменил, помимо всего прочего. Таким образом, использование чистых функций и возвращение новых экземпляров состояния также имеет большое влияние на отладку.

Но главное преимущество, на мой взгляд, заключается в том, что, привязав все входные данные наших компонентов к свойствам состояния, мы можем изменить стратегию обнаружения изменений на push, и это повысит производительность приложения.

Отлично… Итак, каковы преимущества использования NGRX?


Мы уже упоминали большинство из них, когда говорили о принципах redux шаблона. Но давайте отметим наиболее важные преимущества использования redux шаблона в приложении (на мой взгляд):

  • Поскольку у нас есть единый источник правды, и вы не можете напрямую изменить состояние, приложения будут работать более согласованно.
  • Использование redux шаблона дает нам много интересных функций, облегчающих отладку.
  • Тестирование приложений становится проще, поскольку мы вводим простые функции для обработки изменений состояния, а также потому, что оба, ngrx и rxjs, имеют множество замечательных возможностей для тестирования.
  • Как только вы почувствуете себя комфортно при использовании ngrx, понимание потока данных в ваших приложениях станет невероятно простым и предсказуемым.

… и минусы


  • У NGRX, конечно, есть кривая обучения. Она не большая, но и не такая уж и маленькая, и я думаю, что это требует некоторого опыта или глубокого понимания некоторых программных паттернов. Любой разработчик со средним стажем должен быть в порядке, но для младшего поначалу может быть немного запутанным.
  • Для меня это кажется немного многословным. Но каждый раз, когда вы добавляете какое-либо свойство в состояние, вам нужно добавлять действия, диспетчеры, вам может потребоваться обновить или добавить селекторы, эффекты, если таковые имеются, обновить магазин. А также вы запускаете конвейерную (конкатенацию) rxjs операторов и наблюдаемых повсюду.
  • NGRX не является частью угловых библиотек ядра и не поддерживается Google, по крайней мере, напрямую, потому что в команде Angular есть участники ngrx. Просто нужно кое-что обдумать, прежде чем вы добавляете библиотеку, которая будет большой зависимостью для вашего приложения.

Когда стоит использовать NGRX


Таким образом, по общему мнению, ngrx следует использовать в средних/больших проектах, когда управление состоянием начинает становиться сложным в обслуживании. Некоторые люди, более фанатичные, скажут что-то вроде «если у вас есть состояние, то у вас есть NGRX».

Я согласен, что его следует использовать в средних или крупных проектах, когда у вас есть значительное состояние и множество компонентов, использующих это состояние, но вы должны учитывать, что Angular сам по себе предоставляет множество решений для управления состоянием, и если у вас есть сильная команда разработчиков Angular, то, возможно, вам не нужно беспокоиться о ngrx.

При этом я считаю, что сильная команда разработчиков Angular может также решить включить ngrx в решение, потому что они знают всю мощь redux шаблона, а также силу добавляемую операторами rxjs, и они чувствуют себя комфортно, работая с обоими…

Если вы ожидали простого ответа, чтобы решить, когда использовать ngrx, вы не получите его и не доверяете никому, кто дает вам этот ответ, за пределами вашей организации или группы. Решение зависит от изучения плюсов и минусов, понимания вашей команды и учета их мнения.

NGRX Действия, Редукторы, Селекторы, Хранилище, и Эффекты


Это основные строительные блоки потока ngrx. Каждый из них берет на себя часть процесса начала операции по изменению нашего состояния и получению данных.

image

На изображении мы видим поток ngrx. Давайте объясню его…

  1. В наиболее распространенном сценарии все начинается с представления компонентов. Некоторые взаимодействия, осуществляемые пользователем, могут привести к тому, что компонент отправит действие.
    Действия…

    В объекте «Хранилище» имеется функция отправки (запуска) действий. Действия — это классы, реализующие интерфейс NGRX Action. У этих классов Action'ов есть два свойства (возьмем в качестве примера класс действия под названием GetUserName):

    type (тип): это строка только для чтения, описывающая, что означает действие. Например: '[User] Get User Login''.

    payload (полезная нагрузка): тип этого свойства зависит от того, какие данные нужно отправить редуктору. В случае предыдущего примера это будет строка, содержащая имя пользователя. Не все действия должны иметь полезную нагрузку.
  2. Если это действие не вызывает эффекта, редуктор будет анализировать это действие (обычно с помощью оператора switch) и возвращать новое состояние, которое будет результатом слияния старого состояния со значением, измененным вызовом этого действия.
    Редуктор...

    Редукторы — это простые функции, принимающие два аргумента: предыдущее состояние и Действие. При отправке Действие ngrx проходит через все редукторы, передающие в качестве аргументов предыдущее состояние и Действие, в том порядке, в котором редукторы были созданы, до тех пор, пока не найдет аргументы для этого действия.
  3. Если эффект запускается в результате отправки действия, то это происходит из-за того, что перед вызовом редуктора произойдут некоторые побочные эффекты. Вероятно, это может быть что-то вроде вызова службы HTTP для получения данных.
    Эффекты...

    Воздействие на экосистему библиотек ngrx позволяет нам иметь дело с побочными эффектами, вызванными отправкой действия за пределы угловых компонентов или хранилища ngrx.

    Effects слушает, если какое-либо действие отправлено, то, подобно редукторам, он проверяет, является ли действие одним из тех типов действий, которые у него есть.

    Затем выполняется побочный эффект, обычно получение или отправка данных в API.

    В конце концов, редуктор будет выдавать еще одно действие, обычно относящееся к состоянию результата побочного эффекта (success, error и т.д.), затем редуктор войдет в сцену, о чем мы уже упоминали в потоке ngrx.
    После того, как эффект завершен (побочные эффекты закончены), новое действие «состояние-результат» выстреливает из эффекта (может быть, побочные эффекты успешны или неудачны).
  4. Теперь в магазине появилось новое состояние. Состоянием может быть большое дерево объектов, поэтому ngrx вводит селекторы, чтобы иметь возможность использовать только те фрагменты объекта, которые нам нужны в конкретном компоненте.
    Селекторы...

    Как мы упоминали ранее, дерево состояний может стать довольно большим объектом, не имеет смысла размещать весь этот объект в тех местах, где нам нужна только его часть.

    Хранилище NGRX предоставляет нам функцию «выбрать» для получения частей нашего хранилища. Но что, если нам нужно применить некоторую логику к этому срезу, прежде чем использовать данные в компонентах.

    Здесь селекторы принимают меры. Они позволяют нам отделить любое преобразование данных среза состояния от компонентов. Функция выбора «store» принимает в качестве аргумента простую функцию, эта функция является нашим селектором.

    Хранилище ...

    Хранилище — это объект (экземпляр класса Store ngrx), который объединяет вещи, о которых мы упоминали ранее (действия, редукторы, селекторы). Например, когда отправляется действие (с использованием функции отправки объекта хранилища), хранилище является тем, которое находит и выполняет соответствующий редуктор.

    Он также является держателем состоянием приложения.


Продолжение статьи с примерами использования: «Angular: пример использования NGRX».
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 17

    +1
    Мне как-то комфортнее на mobx. Пробовал redux — так себе удовольствие. Постоянное копирование state, особенно в крупном проекте, больно бьет по производительности
      0

      Если речь все еще об Angular — попробуйте ngxs.

        0
        Как понимаю концепция таже, только rxjs в помощь. Mobx выглядит легковеснее. Представление сам понимает при изменении каких observable и computed свойств требуется перерисовка
          +1
          Судя по документации, в NGXS присутствуют все проблемы Redux-подобных сторов: неудобная иммутабельность, необходимость вручную описывать мемоизированные селекторы, обилие бойлерплейт кода. Вот пример из документации:

          export class FeedZebra {
            static readonly type = '[Zoo] FeedZebra';
            constructor(public zebraToFeed: ZebraFood) {}
          }
          
          ...
          
          @Action(FeedZebra)
          feedZebra(ctx: StateContext<ZooStateModel>, action: FeedZebra) {
            const state = ctx.getState();
            ctx.patchState({
              zebraFood: [
                ...state.zebraFood,
                action.zebraToFeed,
              ]
            });
          }
          

          И это всё нужно для того, чтобы запушить значение в массив? На Mobx будет так:

          @action      
          feedZebra(zebraToFeed: ZebraFood) {
            this.zebraFood.push(zebraToFeed)
          }
          


          Mobx в каждом observable значении (в нашем случае zebraFood) хранит список слушателей, которые будут отработать при изменении этого значения. Слушателями будут выступать компоненты, которые рендерят этот observable. На выходе очень малое количество кода и прозрачный принцип работы. Чуть более подробно о недостатках Redux-подобных сторов, писал тут: habr.com/ru/company/inobitec/blog/481288/#comment_21052464
            0

            Конечно, не для того, чтобы запушить в массив. Redux-подобные сторы реализуют паттерн CQRS, с четким разделением запросов и команд. Обработка action-а может быть сколь угодно нетривиальной. У меня, например, был проект, где фронтенд взаимодействовал с rest api и двумя websocket api, без централизованного стейта и потока actions-reducers это бы все легко превратилось в один большой race condition. И возможность просто "отреплеить" экшены очень пригодилась (в первом приближении это что-то похожее на реплей стрима на youtube вместе с чатом). Там, правда, был ngrx (ngxs тогда еще не было, да и ngrx только появился); ngxs, мне кажется, более естественно вписывается в angular.


            Для простых проектов согласен, что mobx уместнее — там добавляемая CQRS сложность не "окупится". Хотя к этой магии с обертками у меня личная неприязнь со времен knockout, но это субъективное :-)

              +1
              Я как-то наоборот думаю). Что именно в крупных проектах mobx удобнее). Как раз к очень крупному проекту применяем. До этого redux пробовали, было много боли и страданий. В мелких проектах, где модель скорее всего простая, я бы применил redux или подобное. Инициализация активной модели mobx забирает больше времени, чем легковесный redux. Только ради этого. Но когда модель большучая, то иммутабельность просто уничтожает производительность
                0
                пришел точно к такому же мнению, пример большого проекта, однокласники, недавно тут была их архитектура приложения
                  0

                  Речь не о размере, а о сложности взаимодействий, модель-то там была относительно небольшая.


                  Впрочем, большие модели — это повод разделить их по разным bounded context. Большая модель, где все со всем жестко связано — это как ни крути ад получится.

                  0
                  Akita вписывается в Angular еще лучше
            +1

            Спасибо за статью, но на мой взгляд, отсутствие примеров с кодом не делает это введение понятнее.

              0
              Часть статьи с примером использования, пока находиться в написании, но в скором времени будет доступна.
              +4
              Мне казалось, что все уже плюются от Redux и используют его только в силу невозможности переписать проект. Это ж тормозное нечно, тянущее за собой кучу однотипных и ненужных «переиспользований кода ровно один раз».
                0
                У меня сейчас на проекте такая архитектура, люди которые это придумали, надеюсь для вас есть отдельный котел! это самое отвратительное решение которое я видел! Angular из коробки умеет держать состояние, для этого достаточно сделать State Injectable, и если вам нужно отслеживать изменение переменно в коде то можно сделать декоратор
                import { BehaviorSubject, Observable } from 'rxjs';
                
                export function observable() {
                    return <T>(target: any, propertyName: string) => {
                
                        Object.defineProperty(target.constructor.prototype, `${propertyName}$`, {
                            get(): Observable<T> {
                                if (!this[`$$_${propertyName}`]) {
                                    this[`$$_${propertyName}`] = new BehaviorSubject(undefined);
                                }
                
                                if (!this[`$$_${propertyName}$`]) {
                                    this[`$$_${propertyName}$`] = this[`$$_${propertyName}`].asObservable();
                                }
                                return this[`$$_${propertyName}$`];
                            }
                        });
                
                        Object.defineProperty(target.constructor.prototype, propertyName, {
                            get(): T {
                                return this[`$$_${propertyName}$`].source.getValue();
                            },
                            set(value: T) {
                                if (this[`${propertyName}$`]) {
                                    this[`$$_${propertyName}`].next(value);
                                }
                            }
                        });
                    };
                }
                


                После чего получить стейт
                @Injectable()
                class AppState {
                
                userId$: Observable<number>;
                
                @observable()
                userId: number;
                }
                


                Затем в компонентах можно просто слушать как обычный Observable
                  0
                  Почему бы явно не использовать BehaviourSubject, вместо этой магии?
                    +1
                    в 90% моих случаев мне достаточно получить текущие значение из стейта и не отслеживать когда оно изменилось, а каждый раз обращаться в .source.getValue(), не очень охото, но это уже вкусовщина, а так да, вполне можно обойтись простым BehaviourSubject
                      0
                      В 90% случаев вообще не нужно получать значение, а нужно получить обсервабл и использовать его в шаблоне.
                      И не надо задумываться отслеживаем мы изменения или нет. Всегда отслеживаем.
                        0
                        у каждого свои случаи )

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое