Pull to refresh

Comments 68

Спасибо за интересную статью.
Появился вопрос. Вы в примере используете:

    authors$: Observable<string[]>;
    unreaded$: Observable<number>;


Но нигде их не инициализировали, вы так и планировали?

И затем Вы их используете:

  this.authors$.next(jokes.map(joke => joke.author));
  this.unreaded$.next(jokes.filter(joke => joke.unread).length);


Но насколько я помню, у Observable нет метода next, он есть у Subject.
Можете объяснить мне это?

Кстати еще, нет такого английского слова unreaded. Есть слово unread.
Нет, просто, видимо, не заметил, когда писал. Код был не реальный. Спасибо за замечание, поправил)
Я бы сделал так:

export class JokesListComponent implements OnInit {
    jokes$: this.jokerService.getJokes().pipe(shareReplay());
    authors$ = this.jokes$.pipe(jokes.map(joke => joke.author));
    unread$ = this.jokes$.pipe(jokes.filter(joke => joke.unread).length);

    constructor(private jokerService: JokerService) {}
}


1) Проще
2) Лениво — пока не отображается в html unread$ — фильтрация не делается
3) Автоматическая отписка, если во время выполнения запроса getJokes() мы перейдём на другой компонент
4) Не используется анти-паттерн subscribe()
Немного неправильный пример, вы забыли про операторы. Ещё вместо `filter(/*...*/).length` я бы предпочел использовать Array.prototype.some, итого:

export class JokesListComponent {
    jokes$: this.jokerService.getJokes().pipe(shareReplay());
    authors$ = this.jokes$.pipe(map(jokes => jokes.map(joke => joke.author)));
    unread$ = this.jokes$.pipe(map(jokes => jokes.some(joke => joke.unread));

    constructor(private jokerService: JokerService) {}
}

Спасибо за комментарий. Видимо, мне нужно было дать более очевидное название для свойства класса unread$, потому что я хотел именно установить именно количество. Array.prototype.some вернет boolean, что не соответствует моей задумке.

Компонент должен выглядеть примерно так
export class JokesListComponent {
  public jokes$ = this.jokesService.getJokes();
  public authors$ = this.jokesService.getAuthors();
  public unread$ = this.jokesService.getUnread();
  
  constructor(
    private jokesService: JokesService,
  ) { }
}


Сабжекты (shareReplay) и запросы в общий сервис. Специфичная для компонента логика — в компонент-скоуп сервис.

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

я к тому что логику выборки нужно переносить в сервис. Для конкретизации гет-методы могут принимать параметры.
И уж тем более не должно быть логики при объявлении полей, максимум один метод.

А можно узнать, когда subscribe стал анти-паттерном? И в чем это выражается?

Не скажу за ангуляр, но в C# Subscribe по хорошему только во вью(т.е на самом верху, в том месте где вам понадобились элементы стрима), а не во View-Model, иначе ваши Observables превратятся из холодных(действия выполняются только при появлении сабскрайбера) в горячие(вью модель и есть этот самый сабскрайбер, который продолжает поглощать эвенты, даже если они никому не нужны).

холодные и горячие отличаются вовсе не этим
medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339
Но вы правы, подписка во вью в Ангуляре осуществляется с помощью AsyncPipe.
Оно вставляет во вью код, который подписывается на поток при инициализации вью и отписывается от него при дестрое.

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


Но частенько бывает, что написать subscribe и правда проще, чем описывать всё то же самое на комбинаторах потоков.

Это я понимаю, что ручное управление подписками не очень в некоторых случаях.
Но чтобы прям анти-паттерном стало…
Лично я использую ngx-take-until-destroy. Чтобы не управлять отписками.

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

Я с вами соглашусь. Спасибо за совет и Ваш пример.
Но думаю что в этом ничего страшного нет.
Я скорее пытаюсь понять почему автор комментария выше — вынес subscribe в антипаттерны.

эт не то чтобы антипаттерн, сколько лишний код с потенциальными ошибками. Например при onPush придется еще вручную вызывать проверку изменений.
И вообще добавляет лишних сущностей, приходится и поток держать и его значение.
Оператор ngx-take-until-destroy скрывает проблему, а не решает её. Большое количество ручных подписок — это императивное управление подписками, в противовес декларативному. Вот хорошая статья на тему: medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
Ok.
Статья какая-то странная, в ней например предлагается использовать так же first, takeWhile, take. Хотя по моему опыту, не всегда может придти событие, в итоге будет висеть подписка…
Но в целом я думаю любой подход, который решает задачу — приемлем.
Я не считаю что take-until-destroy — скрывает проблему, но я с Вами спорить не буду) Удачного дня)
она не странная, она старая.
И там рекомендуют все тот же takeUntil, что и в ngx-take-until-destroy Нетателя Базаля.

Спасибо за уточнение. Согласен с тем, что это действительно проще и эффективнее, если мы используем authors$ и unread$ в шаблоне. Если же нет, то подписки не случится. Пример был придуман, чтобы продемонстрировать реактивность.

jokes$: this.jokerService.getJokes().pipe(shareReplay());
Автоматическая отписка

Автоотписка частично поломана после применения shareReplay().
Возможно, больше подойдет share() (то же самое, но с refCount)

Можете объяснить почему поломана? Вы имеете в виду баг в ангуляре или особенность самого shareReplay?

Отписка в компоненте (через async pipe) произойдет. Но subject, созданный внутри shareReplay, останется активным и подписанным на jokerService.getJokes(). Чтобы этот сабжект отписался от своего источника, когда у сабжекта не осталось подписчиков (то есть когда компонент уничтожается), надо использовать опцию refCount или оператор share(), который по умолчанию использует refCount

Спасибо за интересную статью! Действительно I wish I knew this before...)

Есть вопрос по части DI
Вы говорите:

С местом разобрались, перейдем к самому механизму. Если мы просто указали provideIn: root в сервисе, это будет эквивалентно следующей записи в модуле:

@NgModule({
    // ... здесь другие свойства модуля
    providers: [{provide: JokerService, useClass: JokerService}],
})
export class JokesModule {}



Но в то же время чуть выше есть выражение:
Во все приложение — указываем provideIn: ‘root’ в самом декораторе сервиса.


Правильно понимаю что в вашем примере JokesModule является рутовым? По дефолту рутовым является AppModule, если не указано другое.
Не критично, но может ввести в некоторое заблуждение)

Спасибо, соглашусь. Я имел в виду, что JokesModule является рутовым. Поправил, чтобы не вводить в заблуждение.

Всего 18 строк со всеми красивыми отступами. А теперь попробуйте переписать этот пример на Vanilla или хотя бы на jQuery. Почти 100% у вас это займет как минимум в два раза больше места и будет не так выразительно. Здесь же вы можете просто идти глазами по строке и читать код как книгу.

Не согласен с вами:
1) pipe внутри pipe-a вы серъездно
2) можно сделать еще красивее (но не в рамках ангуляра):
private loadUnreadJokes() {
  this.showLoader(); // Ставим лоадер

  document.addEventListener("DOMContentLoaded", async () => {
    try {
      this.jokes = (await axios.get('/api/v1/jokes')).map((jokes) => // Запрашиваем шутки
        jokes.filter((joke) => joke.unread) // Фильтруем непрочитанные
      )
    } catch (e) {
      /* Обработка ошибки */
    }
    this.hideLoader(); // Скрываем лоадер вне зависимости от результата
  });
}

Спасибо, так действительно получилось очень емко. Правда, в вашем примере используется axios, который много скрывает под капотом. Возможно, можно использовать современный fetch. Кроме возможностей потокового отображения данных, я так же хотел показать, что многое доступно из коробки и не требует лишних зависимостей. Хотя согласен, надо было и это тоже отметить в тексте отдельно.


Если можно, то я бы хотел уточнить на счет пайпа в пайпе. Почему этот подход считается некорректным? Разве в любом switchMap или mergeMap мы не переключаемся на новый поток? Или вы имеете в виду, что нужно вынести это на уровень выше?

Почему этот подход считается некорректным?
потому что получается дополнительная вложенность
    fromEvent(document, 'load')
        .pipe(
            switchMap(() => this.http.get('/api/v1/jokes')),
            map((jokes: any[]) => jokes.filter(joke => joke.unread))
        )

Так же лучше читаемость, не правда ли?

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


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

    fromEvent(document, 'load')
        .pipe(
            switchMap(() => this.http.get('/api/v1/jokes')),
            catchError((err) => {}),
            map((jokes: any[]) => jokes.filter(joke => joke.unread)),
            catchError((err) => {})
        )

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


Это было во-первых, а во-вторых из catchError тоже надо что-то возвращать.


Работать будет только как-то так:


    fromEvent(document, 'load').pipe(
        switchMap(() => this.http.get('/api/v1/jokes').pipe(
            catchError((err) => empty()),
        )),
        map((jokes: any[]) => jokes.filter(joke => joke.unread)),
    )
не забудьте что empty так же немедленно завершит поток.
Не в «одноразовых» потоках он не всегда уместен.
Да согласен, еще один минус rxjs :)

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

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

В модуль — указываем провайдер в декораторе сервиса как provideIn: JokesModule

А вот так делать почти никогда не надо. Честно говоря, я вообще не знаю, когда это можно (и имеет смысл) безопасно сделать.


Проблема в том, что это приводит к циклическим зависимостям.


Ведь зачем нам сервис? Чтобы использовать в компоненте. Стало быть, компонент будет его импортировать. Далее, сам компонент декларируется в модуле — модуль импортирует компонент. И наконец сервис импортирует этот модуль, чтобы сделать providedIn: Module.
Круг замкнулся.

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

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

Сервисы нужны для других сервисов, которые в конечном итоге импортируются компонентами. Просто удлинили цепочку, цикл остался.


модулях, которые поставляют сервисы

Если мы делаем приложение, а не библиотеку, то зачем нужны "модули, которые поставляют сервисы"? providedIn: root отлично работает в этом случае.


В случае с библиотекой это (теоретически) даст чуть больше гибкости в использовании. На практике тоже неочевидно, зачем это может понадобиться.

Сервисы нужны для других сервисов, которые в конечном итоге импортируются компонентами. Просто удлинили цепочку, цикл остался.

Импортируются компонентами из других модулей

Если мы делаем приложение, а не библиотеку, то зачем нужны «модули, которые поставляют сервисы»? providedIn: root отлично работает в этом случае.


например мой сервис зависит от стороннего модуля, который я настрою в модуле через ExternalModule.forRoot()

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

Я согласен, что не совсем очевидно, зачем может понадобиться, предлагаю дальше не углубляться :)
более того. нет смысла не писать providedIn: 'root'
Многие считают что это сразу провайдит сервис в рутовый модуль. Нет, он конечно появится на рут-уровне, но не сразу. это treeshakeble провайдер, ангуляр сам разберется в какой бандл его зашить.
О, вот это интересно! Проводили тесты?
Ну вы посмотрите что в бандлах.
Если рутовый провайдер используется в одном лейзи модуле то он зашивается в ту же лейзилоадед фабрику.
Если в нескольких то возможны варианты, может в common, может в отдельный бандл.
Думаю стоит также добавить, что от подписок на Observable надо на забывать отписыватся

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

Статья хорошая, но вот хотелось бы большего раскрытия про provideres. В статье говориться, что можно провайдить сервис в:
1. В корень
2. Модуль
3. Компонент
Тоже самое говориться и в документации ангуляра.
Вопрос собственно в том, как определить «потребности». Я считаю, что сервис, который используется во всем приложении, например сервис проверки прав пользователя или сервис работы с модальными окнами стоит провайдить в корень. Если в модуле есть несколько компонентов, которые используют один сервис, то стоит провайдить в модуль (например, компоненты для работы с какой-то сущностью и сервис, который предоставляет crud-методы). А если компонент получает данные из сервиса, который был реализован только для него или хранит в сервисе свое состояние, то стоит провайдить сервис в компонент.

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

Можете ли подсказать что можно поизучать в данном направлении, а также насколько моя точка зрения близка к «идеальному» приложению или я совсем не прав

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

Согласен, что нет серебряной пули, но просто хотел узнать, возможно есть какие-то best practices, которых желательно придерживаться и bad practices, которых лучше избегать или вовсе не стоит делать. Не смог найти источников, которые бы описывали какие-либо подходы по организации providers в angular.

Singleton-сервисы — делаем providedIn: root.


Не-singleton (это сервисы с состоянием для компонента из вашего примера) — провайдим в этот компонент.

Есть смысл делать синглтон в рамках модуля, в том случае когда сервис обращается к компонентам объявленным в этом модуле.
Например из рутового сервиса не получится открыть диалоговое окно оверлея cdk с локальным компонентом.
Или в сервисе у нас находится фабрика компонентов.
Например из рутового сервиса не получится открыть диалоговое окно оверлея cdk с локальным компонентом.

Звучит подозрительно. Почему это?

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

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


Но это не повод избегать providedIn: root для сервиса, который отрисовывает его (через MatDialog в нашем случае).

мой неверующий друг, вот, не поленился, сделал демо
stackblitz.com/github/xuxicheta/local-and-root-dialogs

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

Спасибо, добрый человек!


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

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

Спасибо за статью, я бы еще немного зарефакторил метод loadUnreadJokes() для красоты картины


1) цепочка операторов в пайпе без вложенностей, вместо:


.pipe(
     switchMap(
          () =>
              this.http
                  .get('/api/v1/jokes') // Запрашиваем шутки
                  .pipe(map((jokes: any[]) => jokes.filter(joke => joke.unread))), // Фильтруем непрочитанные
    ),
)

это выглядит как then() в then() у промисов:


firstPromise
   .then(() => secondPromise
         .then(() => 'some value')
   );

Поэтому, по бэст практисам лучше делать цепочку без вложенностей (как минимум, меньше текста печатать):


.pipe(
      switchMap( 
          () => this.http.get('/api/v1/jokes') // Запрашиваем шутки 
      ),
      map( 
          // Фильтруем непрочитанные 
          (jokes: any[]) => jokes.filter(joke => joke.unread)
      ),
)

2) не первый раз встречаю в проектах, когда спиннер закрывают на complete() в subscribe(), и есть при этом функция-обработчик ошибок, в которой под капотом тоже происходит закрытие спиннера. С одной стороны это логично, т.к. complete() срабатывает только на успешное выполнение Если же триггерится ошибка, complete() не отрабатывает. Для двух кейсов хорошо работает оператор finalize() https://www.learnrxjs.io/operators/utility/finalize.html


.pipe(
   ... ,
   finalize(() => this.hideLoader())
)
  1. Про вложенность ответил в комментарии выше, но я согласен, что так читается удобнее.
  2. Спасибо, действительно, так даже читаться будет проще.
если изменилось значение Input();
если произошло событие внутри компонента или его потомков;
если проверка была запущена вручную.
CD также дернется когда новое значение пролетает через AsyncPipe используемый в шаблоне компонента. Часто бывает удобно, особенно когда используется реактивный стор (NgRx или самодельный на BehaviourSubject).

Да, все верно. Получение нового значения в потоке разве не является событием внутри компонента, как, например, выполнение промиса?

Событие то происходит не в самом компоненте, компонент только транслирует событие/нотификацию/порцию-данных-в-стриме в шаблон. По моему мнению async pipe нужно указывать отдельным пунктом при перечислении тригерров CD при использовании onPush стратегии тк для новичков это не очевидно.
И еще сам отписывается. Это все понятно, просто по моему мнению async pipe нужно указывать отдельным пунктом тк для новичков это не очевидно.
да, этот коммент предназначался скорее автору топика. Он уже кажется немного перепутал перехват дом-событий и подписки, это следствие ангулярной магии, которая нигде особо не объясняется.

я бегло просморел и мне это показалось пересказом официальной документации

Sign up to leave a comment.