В приложении Angular, над которым я работал последние несколько месяцев, я обнаружил, что мне обычно нужно управлять тремя вещами при работе с любой частью состояния приложения, связанной с получением данных из серверной части. К ним относятся: сам ресурс данные , загружается ли этот ресурс в данный момент загружается и есть ли какие-либо ошибки. Я хотел бы обрисовать, как можно начать с разработки простого приложения, имеющего дело с этим, а затем перейти к тому, как можно придумать различные способы упростить, улучшить или иным образом подойти к этому, особенно как можно попытаться уменьшить шаблон, связанный с NgRx или другими приложениями на основе Redux. Под шаблоном я подразумеваю определение нескольких одинаковых переменных-членов, классов и функций-селекторов для управления одними и теми же вещами снова и снова. Сопутствующее приложение для этой статьи доступно здесь — есть 6 коммитов, представляющих разные вещи, изменяемые на данном этапе рефакторинга, плюс один для первоначального коммита Angular CLI.

Этап 1: начальная функциональность приложения

Код для приложения на этом начальном этапе доступен здесь.

Вначале у нас есть простое приложение для просмотра списка пользователей с двумя представлениями: список пользователей и сведения о пользователе. Счетчик прогресса отображается на каждом до тех пор, пока фактические данные не будут загружены. Это приложение NgRx с одним фрагментом состояния («домен», если хотите): users. В состоянии пользователей мы управляем четырьмя свойствами:
- users, который представляет собой список пользовательских объектов,
- userDetails, который представляет пользовательский объект, который мы в настоящее время хотите увидеть подробности,
- загрузка, которая является логическим значением для того, выполняется запрос к серверу или нет,
- ошибка , который представляет собой ошибку, которую мы потенциально можем получить от сервера в случае, если что-то пойдет не так — свойство состояния для ошибки существует только для полноты картины, мы не управляем ошибками в этом демонстрационном приложении.

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

Для управления получением всех пользователей для представления списка существует 3 типа действий, названных в коде по-разному, как вы увидите ниже, но для иллюстрации мы будем ссылаться на них с этого момента, используя следующие описания:
- perform — отправляется вручную в компонентах контейнера для простоты — устанавливает состояние загрузки в true, состояние данных в false и состояние ошибки в false
успех — генерируется в UsersEffects класс в случае успешного ответа — устанавливает состояние загрузки в false и устанавливает состояние данных в значение полезной нагрузки действия
сбой — генерируется в классе UsersEffects в случае неудачного ответа — устанавливает состояние загрузки в false и состояние ошибки в полезную нагрузку действия
Существует три аналогичных типа действий для управления данными пользователя; здесь перечислены все типы действий:

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

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

Этап 2: знакомство с приборной панелью

Код доступен здесь.

На этом этапе мы представляем панель мониторинга. Один пользователь (всегда тот, у которого id: 1, для простоты) и список всех пользователей отображаются одновременно в этом представлении. Запустите приложение на этом этапе и перейдите к панели инструментов, нажав кнопку в исходном представлении списка. Действия, влияющие на свойства состояния userDetails и users, управляют одним и тем же свойством loading , и наблюдаемое его текущее значение передается обоим компоненты app-user-details и app-users-list:

После завершения запроса списка пользователей (2 секунды) индикатор выполнения исчезает, и отображаются как список, так и пустые сведения о пользователе. Однако запрос сведений о пользователе еще не завершен (4 секунды), поэтому сведения о пользователе пусты, как показано ниже:

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

Этап 3: разделение загрузки и ошибкисвойств состояния
Код здесь.

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

Компонент app-user-details подписывается на наблюдаемое свойство состояния userDetailsLoading , а компонент app-users-list подписывается на наблюдаемое свойство usersLoading государственная собственность. Аналогично у нас есть два отдельных свойства для состояния ошибки и два соответствующих наблюдаемых объекта для них:

Теперь панель сведений о пользователе отображается правильно — счетчик исчезает через 4 секунды, как только сведения о пользователе действительно загружаются.

Несмотря на то, что проблема кажется решенной, что, если в будущем у нас будет гораздо больше, что нужно отображать на панели инструментов — скажем, нам нужно: добавить категорию для пользователя, отобразить премиум-пользователей отдельно, повысить статус одного до премиум-/платного пользователя, удалить пользователя. , отправить электронное письмо, добавить роль… Если требования заключаются в том, чтобы обрабатывать все эти проблемы с помощью отдельной конечной точки бэкэнда и управлять всем этим в нашем едином представлении панели инструментов, и они предписывают отображать отдельный индикатор загрузки и сообщение об ошибке для различных компонентов, связанных с эти различные части состояния, и мы продолжаем идти по этому пути, скоро будет n * 3 свойства состояния — для данных, загрузки и ошибок каждого аспекта состояния приложения. Каждому из них потребуется функция селектора в нашем файле редуктора, они должны быть представлены как переменная в компонентах-контейнерах и переданы в качестве входных данных для любых потребляющих презентационных компонентов. Кроме того, для каждого свойства хранения данных будет три стандартных действия — ранее упомянутые действия «выполнение», «успех» и «неудача» — и в настоящее время у нас есть отдельный класс для представления каждого действия. Это быстрое умножение кода распространено в приложениях на основе избыточности, давайте посмотрим, можем ли мы что-то с этим сделать.

Этап 4: перенос загрузки в класс эффектов ngrx
Код здесь.

usersLoading$ и userDetailsLoding$ — это наблюдаемые объекты логического состояния, которые переключаются в ответ на процесс обработки действий редуктора. На этом этапе мы перемещаем эти логические переменные, представляющие загрузку, из нашей модели состояния и нашего редуктора и пытаемся представить их как наблюдаемые в нашем классе Effects . Такие наблюдаемые будут выдавать true после запуска действия типа «выполнение» и будут принимать значение false, когда выдаются действия «успех» или «неудача». На данном этапе это реализовано — реализована удобная функция для создания наблюдаемой загрузки:

и используется в классе UsersEffects для создания наблюдаемых объектов userDetailsLoading$ и usersLoading$ (только usersLoading$, показанных ниже — то же самое относится к userDetailsLoading $):

Затем они потребляются компонентами контейнера. В декораторе @Effect() мы указываем dispatch:false, так как createLoadingObservable возвращает наблюдаемое значение логического значения и не любого типа Action, и мы не хотим, чтобы логическое значение отправлялось в наш магазин (что привело бы к ошибке invalid action ). Одновременно мы удаляем свойства usersLoading и userDetailsLoading из модели состояния, из логики редуктора и удаляем функции выбора для этих свойств состояния.

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

Этап 5: действия по рефакторингу
Код здесь.

На данном этапе файл действий содержит около 60 строк кода (правда, с довольно большим интервалом), с кодом только для 6 действий и псевдонимов их типов, хотя мы представляем логику в основном для двух вещей: получения всех пользователей и получения сведений о пользователе. Если мы сохраним расстояние, как сейчас, и добавим действия для нескольких вещей, упомянутых ранее, как потенциально запланированных для панели инструментов, файл действий быстро приблизится к 200 строкам.

Когда я попытался свести к минимуму процесс многократного написания трех классов для каждой новой операции, сохраняя при этом безопасность типов, я пришел к решению, аспекты которого изложены здесь: ' и свойство type действия 'failure' будет состоять из свойства type действия 'peform' с соответствующим суффиксом: _SUCCESS для действий 'success' и _FAILUREдля действий "сбой"
— вместо классов действий представлять их как статические функции, создающие действия, помещенные в класс Действия
— представлять каждый тип действия — «успех», «успех» и «неудача» действия как такой статической функции
— определить информацию о типе полезной нагрузки каждого объекта действия, которую должна возвращать каждая такая статическая функция
— создать машинописный текст декоратор для действий «выполнить», который будет определять логику создания объектов действий для всех трех типов действий: «выполнить», «успех» и «неудача», и аннотировать «выполнить» действия с ним:

Код функции декоратора:

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

Но если подумать, тип объединения только гарантирует, что когда мы обращаемся к объекту действия в теле функции-редюсера, мы получаем доступ только к свойствам типа объединения, то есть к свойствам, присутствующим в каждом классе действий, из которого состоит наш тип объединения. В нашем случае остаются только свойства type и payload. Поскольку свойства type и payload являются единственными, доступ к которым осуществляется в редюсерах в 99,9 % случаев, я был бы готов пожертвовать этим и определить тип, который будет использоваться повсюду. приложение для объектов действия — UserAction — и используйте его вместо типа объединения, которого больше нет. Этот тип объявляет, что допустимое действие должно иметь тип и может иметь необязательное свойство payload, которое охватывает подавляющее большинство действий, которые я когда-либо использовал в своих проектах.

Файл действий теперь гораздо менее загроможден и содержит менее 30 строк кода:

Этап 6: действия по рефакторингу
Код здесь.

Мне пришлось немного изменить ситуацию, чтобы AOT не жаловался на использование вызовов функций при инициализации состояния, поэтому я перенес логику создания начального состояния в функцию, которая является фабрикой для провайдера INITIAL_STATE. Поддержка предоставления функции для значения начального состояния появилась в ngrx/store в версии 4.0.5., которая еще не была доступна в npm на момент написания этой статьи.

На этом следующем этапе мы делаем так, чтобы все три «состояния проблем», если хотите (данные, загрузка, ошибка), данное свойство состояния (например, userDetails) представлено в одном объекте. То есть userDetails больше не будет содержать простой объект User — он будет заключен в объект StateItem, содержащий информацию о данных свойства, загрузка и ошибка. Видение состоит в том, что все будущие расширения модели состояния в этом приложении также будут следовать этой методологии.

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

Глядя на код, мы видим, что теперь мы можем хранить только одно свойство в нашей модели состояния для каждого аспекта приложения (например, только под userDetails) вместо двух (отдельные userDetails и userDetailsError), или даже три, как было на этапе №2 (добавьте к ним userDetailLoading). Ниже вы можете увидеть новый интерфейс для состояния, связанного с пользователями:

В результате мы удаляем свойства, представляющие загрузку и ошибку, из модели состояния и редуктора, а также их селекторные функции. Кроме того, у компонентов меньше входных данных, и нам больше не нужны свойства-члены, представляющие загрузку в классе Effect (посмотрите на diff). Поэтому мы можем дополнительно удалить утилиту createLoadingObservable как а также наблюдаемые объекты usersLoading$ и userDetailsLoading$ из класса Effects.

Сводка

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

Затем мы рассмотрели несколько идей о том, какие шаги можно предпринять, чтобы уменьшить количество «повторяющихся кодов», если хотите, связанных с этим процессом, который, по-видимому, присущ приложениям на основе NgRx или вообще Redux. Надеюсь, они вам понравились и вы захотите поэкспериментировать с ними в своих собственных проектах, или, возможно, вы немного вдохновились и уже придумали что-то получше. Если да, или если у вас есть собственные дополнительные техники, которые вы используете ежедневно, есть какие-то возражения, связанные с этими идеями, или вам понравилась статья, не стесняйтесь написать комментарий ниже :)

Если вам понравилась эта статья, подумайте о том, чтобы порекомендовать ее на Medium (хлопайте в ладоши) и поделитесь ею в Twitter, LinkedIn и т. д.