Обрабатываем вызовы api изящным образом
Обрабатываем вызовы api изящным образом
В этой статье я расскажу о способе обработки состояний для вызовов API и сокращения стандартного шаблона путем сбора общей логики в абстракцию. Этот способ мощный, чистый, и менее подвержен ошибкам. В этой статье предполагается, что мы используем NgRx для управления состоянием.
Бьюсь об заклад, что наличие API вызовов являются одним из самых распространенных требований для веб-разработки. Многие приложения имеют массу API вызовов. С точки зрения пользовательского опыта всегда полезно указывать состояние вызова API, например, показывать крутилку или сообщение об ошибке. Я видел много способов моделирования состояния вызова API и нашел одну главную проблему: тяжелые заготовки (boilerplate
), которые обычно вызывают дальнейшие проблемы.
Тяжесть заготовок кода (boilerplate)
Предположим следующие бизнес требования:
1. Отправить запрос API, чтобы получить список сегодняшних новостей.
2. Показать крутилку во время загрузки
3. Показать загруженный список новостей при успешной загрузке.
Многие разработчики проектируют модель состояния с двумя действиями (скажем, LoadNews
и LoadNewsSuccess
, и двумя обработчиками редуктора для изменения состояния loading
и entities
).
Пока что мы не видим здесь никаких проблем. Это крепкий «стандарт».
Допустим, у нас есть 20 (или даже больше) запросов API в этом приложении. Теперь появляются проблемы:
1. Много заготовок кода. Нам нужно обработать состояние API loading
20 раз, сделать 40 действий и 40 обработчиков в редукторах. Это много кода с повторяющейся логикой.
2. Несогласованное именование. Допустим, 20 вызовов API реализованы 4 разработчиками. Они могут иметь разные соглашения об именах. Например, загрузка может называться isLoading
, waiting
, isWaiting
, started
и т. д.
На самом деле, приведенная выше модель состояния API имеет только одно состояние loading
. Однако предполагается, что полный набор будет иметь больше состояний API (о которых пойдет речь в следующем разделе), что сделает предыдущие 2 пункта еще хуже.
Давайте решим эту проблему изящно.
Что такое полный набор состояний?
Полный цикл вызовов API может иметь следующие состояния:
1. Вызов API не запущен
2. Вызов API начался, но ответа пока нет
3. API-вызов получил успешный ответ
4. Вызов API получил ответ об ошибке
Таким образом, мы можем спроектировать общую модель загрузки следующим образом (назовем ее Loadable
):
4 состояния легко сопоставить со значениями 3 полей.
Я бы также создал 4 простых вспомогательных функции для обновления загружаемого состояния. Обратите внимание, что они являются чистыми функциями и возвращают новые объекты loadable
:
Применим loadable
к нашему примеру загрузки списка новостей
Модель
Помимо 3 полей loadable
, нам нужно еще одно состояние для хранения списка новостей, которые мы получили от API. Итак, мы можем предположить следующую модель:
Действия (actions)
Действия остаются такими же, как в соглашениях ngrx.
Редуктор (reducer)
Мы используем редуктор, чтобы изменить состояние в соответствии с 3 различными действиями.
Побочные эффекты (effects)
UI Component
Кода достаточно, чтобы заставить его работать. Тем не менее, это помогает только обеспечить согласованное именование за счет наследования loadable
, и помогает убедиться в правильности изменения состояния с помощью вспомогательных функций. Это действительно не уменьшает шаблон. Представьте, что если у нас есть 20 вызовов API, нам все равно нужно обрабатывать каждое действие (load
, loadSuccess
, loadError
) в каждом из 20 редукторов. И 20 из них имеют одинаковую логику смены состояний. (то есть loading
success
error
)
Абстрактная логика изменения состояния API для редуктора
Давайте определим функцию более высокого порядка withLoadable
, которая принимает в качестве параметров редуктор, три строки типа действия, и возвращает новый улучшенный редуктор.
Таким образом, редуктор для новостей может быть таким:
baseNewsReducer
обрабатывает не loadable
состояния (то есть entities
)
newsReducer
на самом деле будет применять withLoadable
к baseReducer
, чтобы придать baseReducer
немного «магии», т.е. способность автоматически обрабатывать изменения состояния loadable
.
Таким образом, если у нас есть 20 вызовов API, и мы хотим сохранить все 20 * 3 = 60 состояний, мы можем просто применить withLoadable
к 20 базовым редукторам. В 20 базовых редукторах нас не волнует, как должно обновляться состояние loadable
. Таким образом, это экономит нам много времени на ручное обновление состояния API.
Бонус: подключение Loadable
в компонент пользовательского интерфейса
На самом деле Loadable
обеспечивает действительно согласованный контракт, так что он может быть беспрепятственно связан с глобальным компонентом пользовательского интерфейса. Например, я могу создать общий loadable-container
компонента для обработки пользовательского интерфейса загрузки, глобальный интерфейс ошибок. И единственный контракт с внешним миром — это просто Loadable
в @Input
Это позволит нам показывать каждую крутилку/ошибку вызова API, просто используя этот компонент loadable-container
, что также экономит множество кода в шаблонах.
Пожалуйста, посмотрите окончательный код в StackBlitz или в Github Repo. Единственное отличие от кода статьи в том, что он более строго организован, чтобы показать лучшие практики кодирования в реальной жизни. Кроме того, он использует ложный вызов API для получения списка новостей.
Если вы хотите использовать его в своем проекте, у меня есть пакет npm
для вас. Смотрите тут.
Примечание переводчика: примеры кода дополнены комментариями, исправлены незначительные ошибки, текст незначительно сокращён для более литературно красивого перевода.