📑 TLDR:
- Используйте структуру данных RemoteData из библиотеки @ngspot/remote-data для описания данных, запрашиваемых из API.
- Для достижения наилучших результатов используйте RxJS и пользовательский оператор trackRemoteData из библиотеки @ngspot/remote-data-rx.
Вы когда-нибудь писали компонент или сервис, в котором HTTP-вызов выполняется для запроса некоторых данных из API и отображения их пользователю? Это по большей части риторический вопрос — большинство приложений делают это. Существуют общие сценарии, которые необходимо учитывать при работе с удаленными данными.
🤔 Пример наивного подхода
Много раз я писал компоненты и сервисы, где HTTP-вызовы выполняются для запроса некоторых данных. Ранее мой подход использовал императивный стиль кодирования (до того, как я узнал о волшебной силе потоков данных).
Хотя в приведенном выше коде есть много проблем (например, ручная подписка, неиспользование обнаружения изменений OnPush, неиспользование trackBy для цикла ngFor, ошибка из-за потенциального условия гонки и т.д.), приведенный выше код работает. Пожалуйста, пока не обращайте внимания на недостатки. Большинство из них будут устранены к концу статьи, а остальные опущены для простоты.
Далее я понял, что обращение к API занимает время, и мне нужно отобразить какой-нибудь шаблон загрузки, пока данные загружаются. Поэтому я добавил свойство theisLoading, чтобы отслеживать это!
Достаточно просто! Но подождите, что если API вернет ошибку? Я хочу отобразить что-то пользователю в этом случае. Я знаю, как с этим справиться! Поэтому я ввел еще одно свойство: error!
Уффф, три свойства для отслеживания всех возможных вариантов состояния и целая куча кода для поддержания этих трех свойств?! И это только для одного вызова API. А что если вызовов API будет несколько? То, что я имею, тоже не имеет всех возможных состояний. Есть еще одно — случай, когда данные еще не были запрошены.
В приведенном примере данные загружаются автоматически при инициализации компонента, но все может быть иначе. Что если вы хотите вывести на экран подсказку с инструкциями для случая, когда данные еще не были запрошены? Это очень много спагетти-кода!
💡 RemoteData в помощь!
Спагетти-код для обработки всех возможных состояний может быть решен с помощью структуры данных, которая инкапсулирует все эти возможные случаи:
Можно добиться лучшей безопасности типов, если создать специальный тип для каждого из состояний, а затем использовать функцию объединения TypeScript.
Теперь я создам несколько функций-конструкторов, которые будут возвращать RemoteData для каждого возможного состояния запроса (1) не запрошен, (2) загрузка, (3) успех и (4) ошибка).
Когда все это готово, вот переписанный компонент:
Это намного чище! Нужно поддерживать только одно свойство, и это свойство подходит для всех случаев использования. Библиотека @ngspot/remote-data, по сути, была переделана. Не стесняйтесь использовать ее!
Но я могу сделать лучше! Читать дальше.
💪 Использование возможностей RxJS
Помните многочисленные проблемы, упомянутые в начале статьи?
Среди них есть баг, связанный с условием гонки. Если пользователь много раз быстро нажмет на кнопку «Загрузить товары», то будет запущено множество запросов. Есть вероятность, что из-за временных рамок сети ответы на эти запросы будут возвращаться не по порядку. Ответ на запрос, связанный с самым первым щелчком, может вернуться последним. Это означает, что пользовательский интерфейс может отображать не самые свежие данные.
RxJS идеально подходит для работы с асинхронными потоками данных. В нем есть механизмы, позволяющие справиться с подобными ситуациями. Кроме того, он позволяет легко использовать обнаружение изменений OnPush, что повышает производительность вашего приложения и может улучшить общее качество ваших компонентов.
Без лишних слов, вот переписанный компонент, использующий реактивные потоки и структуру данных RemoteData.
Это решение гораздо более надежно. Здесь нет ручных подписок. Данные передаются в шаблон через реактивные потоки с помощью трубы theasync, что позволяет использовать обнаружение изменений OnPush. Наконец, условия гонки обрабатываются с помощью оператора switchMap, который автоматически отменяет все предыдущие запросы в полете и запускает новый.
RxJS позволяет создать пользовательский оператор, используя несколько существующих операторов. Именно это я и сделал в приведенном выше примере — я взял операторы, используемые для обработки загрузки RemoteData, успеха и ошибок, и извлек эти операторы в пользовательский оператор под названием trackRemoteData. Найти оператор trackRemoteData можно в библиотеке @ngspot/remote-data-rx. В него встроена еще пара колокольчиков и свистков.
С ними код становится еще проще.
🧡 Кредит там, где положено кредит
Существует множество подобных решений для работы с удаленными данными. Я пробовал большинство из них, но ни одно не дало мне того набора функций, который я хотел. Вот некоторые из них:
- С чего все началось: «Как Elm уничтожает антипаттерн пользовательского интерфейса».
- https://www.npmjs.com/package/ngx-remotedata
- https://github.com/daiscog/ngx-http-request-state
Тем не менее, эти решения вдохновили меня на создание двух библиотек, которые я теперь использую в большинстве своих проектов. Надеюсь, что и вы найдете их полезными.
Желаю вам счастливого программирования!
👏 Особая благодарность Ане Бока за рецензию на эту статью.