Angular Soviet
Published in

Angular Soviet

Angular/NGRX — ясное и чёткое введение

Оригинал статьи репозиторий GitHub Документация ngrx Справочник RxJS

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

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

Вот что мы рассмотрим:

ngrx это группа библиотек, «вдохновленная» библиотекой Redux, которая, в свою очередь, «вдохновлена» шаблоном Flux. Это означает, что шаблон Redux является упрощенной версией шаблона Flux, а NGRX является версией шаблона redux с использованием Angular и RxJS.

Что я подразумеваю под «angular/rxjs» версией redux

«Angular» — потому что ngrx — это библиотека для использования в Angular приложениях.

«rxjs» — потому что реализация ngrx работает с использованием потоков observables, и различных операторов, предоставляемых «rxjs».

Главная цель этого шаблона — соорудить контейнер для состояний, работающий по строгим правилам. Они основаны на трёх принципах.

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

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

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

Что значит в одном хранилище? Позже мы поговорим о хранилище, но в общем случае оно несёт ответственность за сохранение состояния и применение к нему изменений, когда ему говорят об этом (например, когда отправляется действие - мы рассмотрим это позже).

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

Состояние только для чтения

Вы никогда не изменяете состояние (state) напрямую, вместо этого вы отправляете действия (dispatch actions). Эти действия описывают, что происходит. Например: получение, добавление, удаление, обновление состояния.

Отправка действия? … Мы поговорим об этом позже, но в основном они являются идентификаторами операции в вашем приложении, и могут быть вызваны/отправлены (triggered/dispatched), чтобы попросить приложение выполнить операцию, которую описывает действие.

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

Изменения делаются чистыми функциями

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

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

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

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

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

Но главное преимущество, на мой взгляд, заключается в том, что, привязав все входные данные наших компонентов к свойствам состояния (state), мы можем изменить стратегию обнаружения изменений (angular/zoneJS change detection) на onPush, и это улучшит производительность приложения.

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

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

… и недостатки

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

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

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

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

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

Действия (actions), редукторы (reducer), селекторы (select), хранилище (store) и побочные эффекты (effects) NGRX

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

На этой картинке мы видим жизненный цикл ngrx. Давайте разберём его …

1. В наиболее распространенном сценарии все начинается в представлении компонента (component view). Взаимодействие с пользователем может привести к тому, что компонент отправит действие (dispatch action).

Действия (actions)…
В объекте хранилища (store object) у вас есть функция для отправки/запуска (dispatch/trigger) действий. Действия — это классы, которые реализуют интерфейс действий NGRX/Actions. Эти классы действий имеют два свойства (давайте возьмем в качестве примера класс действия с именем GetUserName):

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

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

2.1. Если действие не вызывает эффект (trigger effect), то редуктор отфильтрует действие (обычно с помощью оператора switch), и вернёт новое состояние, которое будет результатом слияния старого состояния со значением, которое изменилось после вызова действия.

Редукторы (reducers)…

Редукторы — это чистые функции, принимающие два аргумента: предыдущее состояние (state) и действие (action). Когда отправляется действие, ngrx проходит через все редукторы, передавая в качестве аргументов предыдущее состояние и действие, в порядке, в котором редукторы были созданы, пока не найдет обработчик для этого действия.

2.2. Если действие вызвало эффект, то это говорит о необходимости обработки побочных эффектов перед вызовом редуктора. Это может быть что-то вроде вызова службы HTTP для получения данных.

Эффекты (effects)…

Эффекты в экосистеме библиотек ngrx позволяют нам иметь дело с побочными эффектами (прим. пер.: обычно это функция изменения состояния данных), вызванными отправкой действия вне компонентов Angular или хранилища ngrx.

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

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

2.2.1. После того, как эффект отработал (побочные эффекты закончились), он запускает новое действие «состояние-результат» (побочные эффекты могут быть успешными или неудачными), и мы возвращаемся к пункту 2.1.

3. Теперь у хранилища есть новое состояние. Состояние может быть большим деревом — объектом, поэтому ngrx вводит селекторы, чтобы иметь возможность использовать только необходимые фрагменты объекта.

Селекторы (select) …

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

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

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

Хранилище (store)…

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

Пример использования NGRX

Итак, мы закончили с теорией, представив жизненный цикл ngrx и его участников, а теперь пришло время посмотреть на это в действии. Из этого легко сделать ещё одну статью. Но, на мой взгляд, нет смысла объяснять всё то, что мы объяснили без примеров реализации. Так мы можем посмотреть всё в действии, скачать и поиграться с кодом.

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

Вот что это будет:

Итак, приступим …

Установка библиотеки

Мы собираемся использовать Angular Cli для создания проекта, а затем добавить библиотеки ngrx.

Давайте создадим проект:

ng new angular-ngrx —style=scss

Давайте добавим библиотеки ngrx, которые мы будем использовать:

npm install @ngrx/core @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/router-store --save

Мы устанавливаем почти все библиотеки экосистемы ngrx. Названия большинства из них совершенно ясно представляют их назначение. Например, ядро core, хранилище store, эффекты effects. Но есть пара, которая может удивить вас своим предназначением.

Структура папок хранилища

Давайте начнем с обсуждения файловой структуры хранилища. Эта файловая структура и вся конфигурация хранилища должны быть в core модуле вашего приложения. Но в нашем примере у нас его нет, поэтому хранилище будет расположено в app модуле (действия в значительной степени совпадают, если расположить его в core модуле).

Структура папок хранилища

Структура папок представляет фактическую структуру объекта хранилища. У вас будет главная папка с названием «store» и пять вложенных папок, которые представляют каждого из ключевых игроков хранилища: «Actions», «Effects», «Reducers», «Selectors» и «State».

Создание состояния и начальных значений

Как мы уже упоминали ранее, в нашем приложении будет две основных структуры: пользователи users и конфигурация config . Для каждого из них нам нужно создать объект состояния и начальное значение, а также сделать то же самое для состояния приложения app.

Мы создали два интерфейса для определения объекта пользователя и конфигурации. У нас также есть ещё один для результата HTTP запроса получения объекта пользователя — это просто массив IUser.

интерфейс IUser
интерфейс IConfig

Начнём с состояния пользователя: store/state/user.state.ts

Файл описания состояния пользователя

То, что мы сделали здесь очевидно:

Для состояния конфигурации мы делаем то же самое store/states/config.state.ts:

Файл описания состояния конфигурации

Наконец, нам необходимо сгенерировать состояние приложения store/state/app.state.ts

Файл описания состояния приложения

Создание действия

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

Нам нужно создать действия для пользователей и настройки. Начнем с действий пользователя store/actions/user.actions.ts:

Файл с описанием действий пользователя

Так что это хороший пример действий, которые создаются для функции или сущности entity в нашем приложении. Давайте немного пройдемся по коду:

Ну вот всё … создавать действия просто. Давайте посмотрим, как выглядят действия конфигурации store/actions/config.actions.ts:

Файл с описанием действий конфигурации

Здесь нет ничего нового, и вы, скорее всего, теперь можете легко понять содержимое файла.

Отлично, мы уже определили состояние и действия … давайте создадим редукторы!

Создание редукторов (reducers)

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

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

Нам понадобится редуктор для пользователей и другой для конфигурации, но нам также понадобится генерировать редукторы приложений, давайте начнем с рассмотрения редукторов пользователей store/reducers/user.reducers.ts:

Файл с описанием редукторов пользователя

Давайте обсудим реализацию:

И это всё. В редукторе больше ничего нет. Давайте посмотрим на редукторы конфигурации state/reducers/config.reducers.ts:

Файл с описанием редукторов конфигурации

Глядя на этот код, вы, вероятно, легко его понимаете, т. к. мы уже это всё обсуждали.

Наконец, давайте посмотрим на редукторы приложения store/reducers/app.reducers.ts:

Файл с описанием редукторов приложения

Здесь мы добавляем все редукторы reducers в связанный список map, обрабатывающий действия приложения. Мы используем тип ActionReducerMap для добавления проверки типов действий. Позже мы предоставим provide редукторы этого приложения в модуле angular module хранилища.

Отлично!… Теперь у нас есть наше состояние, наши действия, наши редукторы, но нам всё ещё нужны эффекты и селекторы. Давайте добавим наши эффекты …

Создание эффектов

Обязательно прочтите определение «Эффектов», которое мы уже обсуждали в этой статье.

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

Давайте начнем с пользовательских эффектов store/effects/user.effects.ts:

Файл с описанием эффектов пользователя

В этом файле у нас много чего происходит. Давайте попробуем объяснить это:

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

Давайте посмотрим на эффекты конфигурации store/effects/config.effects.ts:

Файл с описанием эффекта конфигурации

И снова, вы, вероятно, чувствуете себя комфортно читая этот код.

Теперь пришло время поговорить о селекторах …

Создание селекторов (selectors)

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

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

Как всегда, давайте сначала посмотрим на пользовательские селекторы store/selectors/user.selector.ts:

Файл с описанием селекторов пользователя

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

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

Вот так создаются селекторы. Давайте посмотрим на хранилище настроек config/selectors/config.selectors.ts:

Файл с описанием состояния конфигурации

Как и раньше, вы чувствуете себя комфортно читая этот код.

Мы уже создали всё необходимое для хранилища, но нам нужно собрать всё это вместе.

Настроить всё вместе

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

Давайте посмотрим модуль приложения:

Файл с описанием модуля приложения

Давайте перечислим то, что необходимо для настройки нашего хранилища:

Первые два шага необходимы, в то время как шаги 3 и 4 я настоятельно рекомендую, но они не являются обязательными.

Теперь мы наконец закончили … и можем использовать хранилище в наших компонентах!

Использование хранилища в некоторых компонентах

Может быть, сейчас вы думаете:

Великий Санти, я потратил 20 минут, читая твою статью, я знаю, что мне нужно знать о ngrx: что это такое, когда его использовать, как его настроить … и это здорово, но не могли бы вы показать мне сейчас, как я могу это использовать!

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

Во-первых, давайте получим конфигурацию при запуске приложения:

Файл с описание компонента приложения

Вот и всё … Мы уже написали код эффекта, который будет обрабатывать это действие, и редуктор, который будет обрабатывать успех этого эффекта. Как только хранилище приобретёт новое состояние, селектор изменит значение нашего свойства. Удивительно!

Вот как мы связываем данные конифгурации в HTML:

Файл с описанием HTML разметки компонента навигации приложения

Как только у config$ изменится значение, мы увидим его в HTML.

Теперь давайте посмотрим список пользователей container/users/users.component.ts:

Я использую концепции компонентов контейнеров и презентационных компонентов (прим. пер.: умные и тупые компоненты). Если вы не знакомы с этим подходом, посмотрите здесь.

Файл с описанием компонента пользователя

HTML выглядит так:

Файл с описанием HTML разметки компонента пользователя

Мы отображаем список пользователей в презентационном(тупом) компоненте, и привязываем выбранного пользователя к функции navigateToUser(), которую мы видели в контейнерном(умном) компоненте пользователя ранее.

Отлично… а как мы показываем выбранного пользователя?…

Давайте посмотрим на компонент пользовательского контейнера:

Файл с описанием контейнерного компонента пользователя

Этот компонент получает параметр id из ActivatedRoute (текущего url), а с остальным вы, вероятно, уже знакомы. Отправление id в качестве параметра, выбор выбранного пользователя…

Если вы хотите увидеть весь код, просто зайдите в репозиторий GitHub.

Для отладки вы можете использовать инструменты разработчика, которые довольно просты в использовании… но данная статья и так уже достаточно подробная, и, я надеюсь, что вы без труда разберетесь с этими инструментами.

Заключение

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

Мы начали объяснять понятия, разбираться, как это работает, зачем оно используется, и, наконец, мы рассмотрели полный базовый пример.

Вот репозиторий GitHub…

Я рекомендую вам скачать его и немного поиграться с кодом.

Я действительно надеюсь, что эта статья поможет вам понять принципы работы ngrx.

Немного благодарностей…

Спасибо @leosvel.perez.espinosa за то, что он уделил некоторое время обсуждению со мной некоторых функций ngrx и @fevialmeida за этот невероятный баннер!

Примечание переводчика: примеры кода дополнены комментариями, исправлены незначительные ошибки, текст незначительно сокращён для более литературно красивого перевода.

--

--

Hello, fellow comrade. Take off your fufaika, valenoks and the shapka-ushanka with the red star. Take a drink of vodka right from the samovar and be ready to hear a story about expression changed after it has been checked error.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store