Что привлекает в Ангуляре экспертов и удручает начинающих? Одно и тоже, RxJS.
Почему же это так сложно для начинающих? Одна из причин - есть огромное количество операторов, которые нужно просто знать, и без поиска понимать в чем разница между concatMap, switchMap и mergeMap. Почему же это так нравится тем, кто это уже изучил? Потому что вы начинаете понимать все могуществао RxJS, когда парой операторов вы можете сделать то, что в императивном коде писали бы полдня на двух страницах. Ведь это так приятно, ощущать себя богом, когда код просто отскакивает от ваших пальцев, а вы радостно рассказываете коллегам как вы классно и главное просто решили задачу.
Вместе с тем каждый помнит, что вызывать множественные subscribe плохо (как и вообще подписываться), вызывать сайд эффекты из tap (хотя конечно все так делают). А что же чаще всего забывают с RxJS? Ошибки. Какие ошибки? Лично я выделяю всего два типа ожидаемые и неожиданные.
Неожиданные ошибки - это те ошибки, которые возникают в результате плохого кода либо неожиданного JSON-ответа сервера - деление на ноль, обращение к null-объекту, некорректный парсинг JSON. Можно привести много примеров, главное то, что это полноценные ошибки, которые требуют изменений в вашем коде.
Ожидаемые ошибки - это ошибки, которые мы можем предусмотреть в работе приложения, самый простой пример - это упавший сервер с данными. Тут мы как программисты в своем коде ничего не правим, а обычно идем ругаться в другую команду.
Данные типы ошибок, я считаю, надо разделять и в последствиях - в случае первых красиво падать, в случае вторых - уметь с ними работать и показывать нашим пользователям что-то, из чего можно сделать красивый скриншот “виноваты они а не мы”.
Давайте создадим простое Ангуляр-приложение, которое будет грузить данные пользователя github, а потом (не)красиво но ожидаемо падать при попытке загрузки несуществующего.
Код, соответственно, выглядит вот так:
Это довольно простое приложение, падающее при попытке сделать что-то, о чем мы не подумали (в данном случае - загрузить несуществующего пользователя). Печально, но визуально это будет выглядеть в лучшем случае как неработающая кнопка, в худшем - все, что было основано на таком коде, также упадет.
Попыток решения ошибок способами RxJS много, предлагаю изучить в прекрасной статье RxJs Error Handling: Complete Practical Guide
В Ангуляре я чаще всего видел решение через определение в модели компонента отдельных полей data, error и isLoading.
В нашем случае темплейт разрастается на каждую ветку
а в коде тайпскрипта приходится делать все поля опциональными, добавляя головной боли при использовании данных в последующих вызовах других сервисов.
Почему это (а также другие способы решения) плохо предлагаю посмотреть еще и в оригинальном ролике от Артём Кобзаря и Дмитрия Махнёва (не|ну)жная монада Either на практике и в теории. В действительности мы о таких ожидаемых ошибках просто забываем до тех пор, пока не упадет, и решаем такие проблемы точечно. Я же предлагаю изначально считать ошибки полноценными жителями нашего города и никогда не верить бэкенду, даже если мы счастливые обладатели звания FullStack Developer.
Давайте использовать такое представление данных, которое само по себе покажет вам, что в нем могут быть как данные, так и ожидаемая ошибка. Встречайте,
В общем случае это union тип, слева - ошибка, справа - правильные данные.
Впрочем, самописный тип я предлагаю не использовать, а взять готовый. Многие берут тип из библиотеки fp-ts, я же предлагаю взять более легковесный и понятный @sweet-monads/either.
Для начала давайте напишем простой RxJS оператор, который будет автоматически ловить ошибки сети и преобразовывать их в структуру ошибки. Для простоты давайте сохранять только текст ошибки - решение состояние прогресса я рассмотрю в следующей статье.
В чём прелесть такого оператора как отдельной единицы в в приложении - то что мы можем его применять точечно, в отдельных местах приложения, а не мигрировать целиком. Также такой оператор отделяет ожидаемые ошибки от неожиданных.
Код с использованием этого оператора будет выглядеть вот так
Показывать же данные в html лучше через создание специальных директив и компонент-врапперов (также оно в альфа-версии доступно в npm как пакет) Приводить их тут не буду, предлагаю изучить сразу обновленный пример, включая код, по этой ссылке.
Основные изменения - появление компонента Either и использование правых данных через структурную директиву ifRight.
Теперь при ожидаемом падении мы покажем текст ошибки, а в случае загруженных данных будем работать с ними.
Заключение
Как видите, здесь мы уверены что ожидаемая ошибка будет обработана и показана, особенно когда на проекте используются кодогенераторы.
Комментарии (10)
shai_hulud
18.11.2022 17:35Вроде в туториалах ангуляра вот такой подход используется. Чем он не устроил?
<ng-template [ngIf]="profile$ | async as profile; else profileLoadingOrError"> {{profile}} </ng-template> <ng-template #profileLoadingOrError> <ng-template [ngIf]="profileError$ | async as error; else profileLoading"> {{error}} </ng-template> </ng-template> <ng-template #profileLoading> Loading... </ng-template>
profile$ = ...; // .pipe(shareReplay(1)) profileError$ = profile$.pipe(catchError(error => of(error)));
Покрывает все три состояния лоадинг/ошибка/результат и лишнего бойлерплейта почти нет.
Всё это конечно можно попробоывать завернуть в одну структурную директиву и вообще будет красиво.
Lonli-Lokli Автор
18.11.2022 18:23Этот подход и есть пример из статьи, который мне не нравится.
Он громоздкий как в темплейте, так и в коде
Его придется вручную проверять в случае, когда на основе первых данных придется делать вызовы для загрузки следующих - if (error) else
-
Придется постоянно держать в уме все случаи, когда надо менять флажки - тот же isLoading не может быть true в случае когда данные загружены, а модель это обычно позволяет.
Дополню только что данная статья только про важность ошибок и способа работы с ними, а вот структурная директива с поддержкой лоадинга и прочего будет в следующей статье
nin-jin
20.11.2022 07:19В том же Ангуляре можно писать вообще так:
<div *showWaitingIndicatorAndErrorMessageAutomatically> <div>Name: {{name}}</div> <div>Age: {{age}}</div> </div>
get data() { return fetchJSON( '/profile' ) } get name() { return this.data.name } get age() { return currentYear - this.data.birth.year }
Почему все так не делают, для меня остаётся загадкой. Возможно дело в стокгольмском синдроме из-за RxJS.
nin-jin
20.11.2022 07:01-1Почему же это так нравится тем, кто это уже изучил?
Я изучил, но мне не нравится. Что со мной не так?
Потому что вы начинаете понимать все могуществао RxJS, когда парой операторов вы можете сделать то, что в императивном коде писали бы полдня на двух страницах.
const FilterSource = new BehaviorSubject< null | ( toy: Toy )=> boolean >( null ) const Filter = FilterSource.pipe( distinctUntilChanged() , debounce(0) , shareReplay(1) ) const ToysSource = new BehaviorSubject( [] ) const Toys = ToysSource.pipe( distinctUntilChanged() , debounce(0) , shareReplay(1) ) const ToysFiltered = Filter.pipe( switchMap( filter => { if( !filter ) return Toys return Toys.pipe( map( toys => toys.filter( filter ) ) ) } ) , distinctUntilChanged() , debounce(0) , shareReplay(1) , )
class Toys { @mem filter( next = null as null | ( toy: Toy )=> boolean ) { return next } @mem toys( next = [] ){ return next } @mem toysFiltered() { if( !this.filter() ) return this.toys() return this.toys().filter( this.filter() ) } }
в случае первых красиво падать, в случае вторых - уметь с ними работать и показывать нашим пользователям что-то
А можно в обоих случаях не падать, а продолжать корректную работу?
Код, соответственно, выглядит вот так:
Шёл 2022 год, шаблоны всё ещё писали без подсветки синтаксиса. И выкладывали в виде скриншотов.
Lonli-Lokli Автор
20.11.2022 15:32Я изучил, но мне не нравится. Что со мной не так?
Полагаю то, что вы эксперт mol, а не Angular.
Шёл 2022 год, шаблоны всё ещё писали без подсветки синтаксиса. И выкладывали в виде скриншотов.
Подсветка везде есть, насколько вижу, да и код у меня через редактор хабра получался больше по размеру, поэтому скриншоты.
Ссылки на рабочий код, который можно скопировать, также везде присутствуют.
avvensis
20.11.2022 12:07Подскажите, чем
either
отличается от оператора materialize()?Lonli-Lokli Автор
20.11.2022 15:28Полагаю имелся в виду eitherify оператор? Потому что сама структура данных отличается фактически всем :)
materialize вообще используется крайне редко, но вот например семпл в чем его отличие от eitherify - вместо 5 next + 1 complete будет значительно больше. Основное отличие в документации - When the source Observable emits
error
, the output will emitnext
as a Notification of type "error", and thencomplete
. То есть на каждую ошибку вы будете опять делать complete вместо продолжения потока. Кроме того, ответ будет изначально одинаковый для разных HttpErrorCode, в то время как eitherify каждый может подправить под себя исходя из того, какой код пришел - 400, 401, 403, 500
dopusteam
Немного странная статья, вначале говорится, что операторов много и тяжело запомнить, потом непонятный переход к ошибкам, потом предложение использовать стороннюю либу без каких либо объяснений (давайте добавим ещё один оператор, а то что то их мало).
На самом деле описанный подход не учитывает, что помимо данных и ошибки у нас часто есть ещё некий флаг, говорящий о том, что данные грузятся или уже загружены.
Можете рассказать, сколько потребуется правок, чтобы добавить в разметку блок, который будет отображаться в момент загрузки данных?
alexshipin
Прочитав статью (заметку?), я так и не понял, что хотел донести до нас автор: что у него не реализовано отслеживание статуса загрузки, или то, что новая библиотека поможет с решением того, что можно решить без это библиотеки?
Всегда в таких случаях возникает вопрос: зачем тянуть ещё библиотеки, если половину из описываемого можно решить без них?
Пользуюсь активно Rx (для JS и C#) - каких-либо проблем не наблюдаю с загрузкой данных... То есть, я могу сказать, что проблема искусственная(?)..
Lonli-Lokli Автор
Это будет в следующей статье, и там для этого будет и новый контейнер, и другой компонент. Текст постараюсь сегодня подправить, чтобы переход был понятен.