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.
Можете объяснить мне это?
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()
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) {}
}
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 в антипаттерны.
Статья какая-то странная, в ней например предлагается использовать так же first, takeWhile, take. Хотя по моему опыту, не всегда может придти событие, в итоге будет висеть подписка…
Но в целом я думаю любой подход, который решает задачу — приемлем.
Я не считаю что take-until-destroy — скрывает проблему, но я с Вами спорить не буду) Удачного дня)
Спасибо за уточнение. Согласен с тем, что это действительно проще и эффективнее, если мы используем authors$
и unread$
в шаблоне. Если же нет, то подписки не случится. Пример был придуман, чтобы продемонстрировать реактивность.
jokes$: this.jokerService.getJokes().pipe(shareReplay());
Автоматическая отписка
Автоотписка частично поломана после применения shareReplay()
.
Возможно, больше подойдет share()
(то же самое, но с refCount)
Можете объяснить почему поломана? Вы имеете в виду баг в ангуляре или особенность самого shareReplay?
Отписка в компоненте (через async pipe) произойдет. Но subject, созданный внутри shareReplay, останется активным и подписанным на jokerService.getJokes(). Чтобы этот сабжект отписался от своего источника, когда у сабжекта не осталось подписчиков (то есть когда компонент уничтожается), надо использовать опцию refCount или оператор share(), который по умолчанию использует refCount
Есть вопрос по части DI
Вы говорите:
С местом разобрались, перейдем к самому механизму. Если мы просто указали provideIn: root в сервисе, это будет эквивалентно следующей записи в модуле:
@NgModule({ // ... здесь другие свойства модуля providers: [{provide: JokerService, useClass: JokerService}], }) export class JokesModule {}
Но в то же время чуть выше есть выражение:
Во все приложение — указываем provideIn: ‘root’ в самом декораторе сервиса.
Правильно понимаю что в вашем примере JokesModule является рутовым? По дефолту рутовым является AppModule, если не указано другое.
Не критично, но может ввести в некоторое заблуждение)
Всего 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)),
)
Толковая статья про фундамент ангуляра получилась, выгодно отличается от большинства статей, в которых только трюки и приёмы описывают. Возможно, стоило бы ещё про ленивые модули пару абзацев накидать и про реактивные формы.
В модуль — указываем провайдер в декораторе сервиса как provideIn: JokesModule
А вот так делать почти никогда не надо. Честно говоря, я вообще не знаю, когда это можно (и имеет смысл) безопасно сделать.
Проблема в том, что это приводит к циклическим зависимостям.
Ведь зачем нам сервис? Чтобы использовать в компоненте. Стало быть, компонент будет его импортировать. Далее, сам компонент декларируется в модуле — модуль импортирует компонент. И наконец сервис импортирует этот модуль, чтобы сделать providedIn: Module.
Круг замкнулся.
Не все сервисы нужны для компонентов, некоторые могут быть нужны для других сервисов из других модулей
Сервисы нужны для других сервисов, которые в конечном итоге импортируются компонентами. Просто удлинили цепочку, цикл остался.
модулях, которые поставляют сервисы
Если мы делаем приложение, а не библиотеку, то зачем нужны "модули, которые поставляют сервисы"? providedIn: root
отлично работает в этом случае.
В случае с библиотекой это (теоретически) даст чуть больше гибкости в использовании. На практике тоже неочевидно, зачем это может понадобиться.
Сервисы нужны для других сервисов, которые в конечном итоге импортируются компонентами. Просто удлинили цепочку, цикл остался.
Импортируются компонентами из других модулей
Если мы делаем приложение, а не библиотеку, то зачем нужны «модули, которые поставляют сервисы»? providedIn: root отлично работает в этом случае.
например мой сервис зависит от стороннего модуля, который я настрою в модуле через ExternalModule.forRoot()
Т.е. может быть кейс, когда наш модуль помимо предоставления сервиса, подключает в себя другой сервис и осуществляет какую то настройку.
Я согласен, что не совсем очевидно, зачем может понадобиться, предлагаю дальше не углубляться :)
Многие считают что это сразу провайдит сервис в рутовый модуль. Нет, он конечно появится на рут-уровне, но не сразу. это treeshakeble провайдер, ангуляр сам разберется в какой бандл его зашить.
1. В корень
2. Модуль
3. Компонент
Тоже самое говориться и в документации ангуляра.
Вопрос собственно в том, как определить «потребности». Я считаю, что сервис, который используется во всем приложении, например сервис проверки прав пользователя или сервис работы с модальными окнами стоит провайдить в корень. Если в модуле есть несколько компонентов, которые используют один сервис, то стоит провайдить в модуль (например, компоненты для работы с какой-то сущностью и сервис, который предоставляет crud-методы). А если компонент получает данные из сервиса, который был реализован только для него или хранит в сервисе свое состояние, то стоит провайдить сервис в компонент.
Вопрос связан с тем, что в команде разработке резко решили, что нужно провайдить в корень и в компоненты. В результате получается, что в один момент может быть несколько инстансов одного и того же сервиса, хотя можно было сделать один провайдинг в модуль.
Можете ли подсказать что можно поизучать в данном направлении, а также насколько моя точка зрения близка к «идеальному» приложению или я совсем не прав
Спасибо! Думаю, что здесь не существует серебряной пули и решать, в реальности, следует команде, так как очень многое зависит от принятых норм и правил. Тема с провайдингом сервисов в принципе довольно интересная и может существовать много разных мнений.
Singleton-сервисы — делаем providedIn: root.
Не-singleton (это сервисы с состоянием для компонента из вашего примера) — провайдим в этот компонент.
Например из рутового сервиса не получится открыть диалоговое окно оверлея cdk с локальным компонентом.
Или в сервисе у нас находится фабрика компонентов.
Например из рутового сервиса не получится открыть диалоговое окно оверлея cdk с локальным компонентом.
Звучит подозрительно. Почему это?
Пока это единственный кейс, когда приходится провайдить сервис к модуль, который я встречал в повседневной жизни.
Так постоянно это делаю. Все диалоги вызываются через глобальный синглтон сервис.
Понятно, что модуль с локальным компонентом все равно надо импортировать.
Но это не повод избегать providedIn: root для сервиса, который отрисовывает его (через MatDialog в нашем случае).
stackblitz.com/github/xuxicheta/local-and-root-dialogs
Придумал еще один кейс, когда провайдер в модуле может быть уместен: когда у сервиса есть внешняя зависимость, которую вот в этом модуле надо подменить.
Придется или делать глобальную фабрику или вот провайдером на уровне модуля.
Спасибо, добрый человек!
Действительно, демо воспроизводит все, что надо. Удивился, полез в недра нашего проекта — как же там работает-то? Оказалось, что мы таки в каждом модуле провайдим этот сервис.
Спасибо за статью, я бы еще немного зарефакторил метод 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())
)
- Про вложенность ответил в комментарии выше, но я согласен, что так читается удобнее.
- Спасибо, действительно, так даже читаться будет проще.
если изменилось значение Input();CD также дернется когда новое значение пролетает через AsyncPipe используемый в шаблоне компонента. Часто бывает удобно, особенно когда используется реактивный стор (NgRx или самодельный на BehaviourSubject).
если произошло событие внутри компонента или его потомков;
если проверка была запущена вручную.
Да, все верно. Получение нового значения в потоке разве не является событием внутри компонента, как, например, выполнение промиса?
github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L143
я бегло просморел и мне это показалось пересказом официальной документации
5 вещей, которые я бы хотел знать, когда начинал использовать Angular