Что привлекает в Ангуляре экспертов и удручает начинающих? Одно и тоже, RxJS.

Почему же это так сложно для начинающих? Одна из причин - есть огромное количество операторов, которые нужно просто знать, и без поиска понимать в чем разница между concatMap, switchMap и mergeMap. Почему же это так нравится тем, кто это уже изучил? Потому что вы начинаете понимать все могуществао RxJS, когда парой операторов вы можете сделать то, что в императивном коде писали бы полдня на двух страницах. Ведь это так приятно, ощущать себя богом, когда код просто отскакивает от ваших пальцев, а вы радостно рассказываете коллегам как вы классно и главное просто решили задачу.

Вместе с тем каждый помнит, что вызывать множественные subscribe плохо (как и вообще подписываться), вызывать сайд эффекты из tap (хотя конечно все так делают). А что же чаще всего забывают с RxJS? Ошибки. Какие ошибки? Лично я выделяю всего два типа ожидаемые и неожиданные.

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

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

Данные типы ошибок, я считаю, надо разделять и в последствиях - в случае первых красиво падать, в случае вторых - уметь с ними работать и показывать нашим пользователям что-то, из чего можно сделать красивый скриншот “виноваты они а не мы”.

Давайте создадим простое Ангуляр-приложение, которое будет грузить данные пользователя github, а потом (не)красиво но ожидаемо падать при попытке загрузки несуществующего.

StackBlitz Demo

Код, соответственно, выглядит вот так:

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

Попыток решения ошибок способами RxJS много, предлагаю изучить в прекрасной статье RxJs Error Handling: Complete Practical Guide

В Ангуляре я чаще всего видел решение через определение в модели компонента отдельных полей data, error и isLoading.

В нашем случае темплейт разрастается на каждую ветку

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

StackBlitz Demo

Почему это (а также другие способы решения) плохо предлагаю посмотреть еще и в оригинальном ролике от Артём Кобзаря и Дмитрия Махнёва (не|ну)жная монада Either на практике и в теории. В действительности мы о таких ожидаемых ошибках просто забываем до тех пор, пока не упадет, и решаем такие проблемы точечно. Я же предлагаю изначально считать ошибки полноценными жителями нашего города и никогда не верить бэкенду, даже если мы счастливые обладатели звания FullStack Developer.

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

В общем случае это union тип, слева - ошибка, справа - правильные данные.

Впрочем, самописный тип я предлагаю не использовать, а взять готовый. Многие берут тип из библиотеки fp-ts, я же предлагаю взять более легковесный и понятный @sweet-monads/either.

Для начала давайте напишем простой RxJS оператор, который будет автоматически ловить ошибки сети и преобразовывать их в структуру ошибки. Для простоты давайте сохранять только текст ошибки - решение состояние прогресса я рассмотрю в следующей статье.

В чём прелесть такого оператора как отдельной единицы в в приложении - то что мы можем его применять точечно, в отдельных местах приложения, а не мигрировать целиком. Также такой оператор отделяет ожидаемые ошибки от неожиданных.

Код с использованием этого оператора будет выглядеть вот так

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

 Основные изменения - появление компонента Either и использование правых данных через структурную директиву ifRight.

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

Заключение

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

Комментарии (10)


  1. dopusteam
    18.11.2022 08:37

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

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

    Можете рассказать, сколько потребуется правок, чтобы добавить в разметку блок, который будет отображаться в момент загрузки данных?


    1. alexshipin
      18.11.2022 09:01
      +2

      Прочитав статью (заметку?), я так и не понял, что хотел донести до нас автор: что у него не реализовано отслеживание статуса загрузки, или то, что новая библиотека поможет с решением того, что можно решить без это библиотеки?

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

      Пользуюсь активно Rx (для JS и C#) - каких-либо проблем не наблюдаю с загрузкой данных... То есть, я могу сказать, что проблема искусственная(?)..


    1. Lonli-Lokli Автор
      18.11.2022 09:03

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


  1. 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)));

    Покрывает все три состояния лоадинг/ошибка/результат и лишнего бойлерплейта почти нет.

    Всё это конечно можно попробоывать завернуть в одну структурную директиву и вообще будет красиво.


    1. Lonli-Lokli Автор
      18.11.2022 18:23

      Этот подход и есть пример из статьи, который мне не нравится.

      1. Он громоздкий как в темплейте, так и в коде

      2. Его придется вручную проверять в случае, когда на основе первых данных придется делать вызовы для загрузки следующих - if (error) else

      3. Придется постоянно держать в уме все случаи, когда надо менять флажки - тот же isLoading не может быть true в случае когда данные загружены, а модель это обычно позволяет.

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


    1. 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.


  1. 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 год, шаблоны всё ещё писали без подсветки синтаксиса. И выкладывали в виде скриншотов.


    1. Lonli-Lokli Автор
      20.11.2022 15:32

      Я изучил, но мне не нравится. Что со мной не так?

      Полагаю то, что вы эксперт mol, а не Angular.

      Шёл 2022 год, шаблоны всё ещё писали без подсветки синтаксиса. И выкладывали в виде скриншотов.

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

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


  1. avvensis
    20.11.2022 12:07

    Подскажите, чем either отличается от оператора materialize()?


    1. Lonli-Lokli Автор
      20.11.2022 15:28

      Полагаю имелся в виду eitherify оператор? Потому что сама структура данных отличается фактически всем :)

      materialize вообще используется крайне редко, но вот например семпл в чем его отличие от eitherify - вместо 5 next + 1 complete будет значительно больше. Основное отличие в документации - When the source Observable emits error, the output will emit next as a Notification of type "error", and then complete. То есть на каждую ошибку вы будете опять делать complete вместо продолжения потока. Кроме того, ответ будет изначально одинаковый для разных HttpErrorCode, в то время как eitherify каждый может подправить под себя исходя из того, какой код пришел - 400, 401, 403, 500

      Ссылка на stackblitz