company_banner

5 вещей, которые я бы хотел знать, когда начинал использовать Angular

    Современный Angular — это мощный фреймворк с множеством возможностей, вместе с которыми приходят и сложные, на первый взгляд, концепции и механизмы. Особенно это заметно тем, кто только начал работу как во фронтэнде в принципе, так и с Angular в частности.


    С этой же проблемой столкнулся и я, когда примерно два года назад пришел в Тинькофф на позицию Junior Frontend Developer и погрузился в мир Angular. Поэтому предлагаю вам короткий рассказ о пяти вещах, понимание которых очень облегчило бы мою работу на первых порах.



    Dependency Injection (DI)


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


    Предлагаю сразу разобраться на примере, а для этого нам понадобится класс. Если в «обычном» JavaScript ООП присутствует с определенными «хаками», то вместе с ES6 появился и «настоящий» синтаксис. В Angular прямо из коробки используется TypeScript, в котором синтаксис примерно такой же. Поэтому далее предлагаю использовать его.


    Представим, что в нашем приложении существует класс JokerService, который управляет шутками. Метод getJokes() возвращает список шуток. Допустим, мы его используем в трех местах. Как получить шутки в трех разных местах в коде? Есть несколько способов:


    1. Создавать экземпляр класса в каждом месте. Но зачем нам засорять память и создавать столько одинаковых сервисов? А если мест будет 100?
    2. Сделать метод статическим и получать данные с помощью JokerService.getJokes().
    3. Реализовать один из паттернов проектирования. Если нам нужно, чтобы сервис был один на все приложение, то это будет Singleton. Но для этого нужно написать новую логику в классе.

    Итак, у нас есть три вполне рабочих варианта. Первый нам не подойдет — в данном случае он неэффективен. Мы не хотим создавать лишние экземпляры, так как они будут полностью идентичны. Остается два варианта.


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


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


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


    Если вы подумали, что можно просто создать объект и по ключу брать нужный сервис, то могу вас поздравить: вы только что поняли, как в целом работает Dependency Injection. Но давайте немного углубимся.


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


    Dependency Injection в Angular


    Как гласит документация, DI — важный паттерн дизайна приложения. Angular имеет собственный фреймворк для зависимостей, который используется в самом Angular для повышения эффективности и модульности.


    Если говорить в общих понятиях, то Dependency Injection — это мощный механизм, при котором класс получает необходимые зависимости откуда-то извне, а не создает экземпляры самостоятельно.


    Пусть синтаксис и файлы с расширением html вас не путают. Каждый компонент в Angular — это обычный объект JavaScript, экземпляр класса. Если говорить общими словами: когда вы вставляете компонент в шаблон — создается экземпляр класса компонента. Соответственно, в этот момент можно передать в конструктор нужные зависимости. А теперь рассмотрим пример:


    @Component({
        selector: 'jokes',
        template: './jokes.template.html',
    })
    export class JokesComponent {
        private jokes: Observable<IJoke[]>;
    
        constructor(private jokerService: JokerService) {
            this.jokes = this.jokerService.getJokes();
        }
    }

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


    Providers


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


    @Injectable({
        providedIn: 'root', // Здесь мы указываем, куда будет «подставлен» сервис
    })
    export class JokerService {
        getJokes(): Observable<IJoke[]> {
            // Наша логика получения шуток
        }
    }

    Когда сервис один на все приложение, будет достаточно этого варианта. Но что делать, если у нас есть, допустим, две реализации JokerService? Или просто по какой-то причине в определенном компоненте нужен свой экземпляр сервиса? Ответ прост: provider.


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


    • Во все приложение — указываем provideIn: ‘root’ в самом декораторе сервиса.
    • В модуль — указываем провайдер в декораторе сервиса как provideIn: JokesModule или в декораторе модуля @NgModule как providers: [JokerService].
    • В компонент — указываем провайдер в декораторе компонента, как в модуле.

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


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

    Это можно прочитать примерно так: «Если запрашивается JokerService, то отдай экземпляр класса JokerService». Отсюда можно получить определенный экземпляр различными способами:


    • По токену — нужно указать InjectionToken и получать сервис по нему. Обратите внимание, что в примерах ниже в provide можно передать этот же токен:


      const JOKER_SERVICE_TOKEN = new InjectionToken<string>('JokerService');
      
      // ... Здесь модуль или компонент
      
      [{provide: JOKER_SERVICE_TOKEN, useClass: JokerService}];

    • По классу — можно подменить класс. Например, мы будем просить JokerService, а отдавать — JokerHappyService:


      [{provide: JokerService, useClass: JokerHappyService}];

    • По значению — можно сразу вернуть нужный экземпляр:


      [{provide: JokerService, useValue: jokerService}];

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


      [{provide: JokerService, useFactory: jokerServiceFactory}];


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


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


    Небольшой итог


    Для полного понимания предлагаю рассмотреть упрощенный механизм Dependency Injection в Angular по шагам на примере сервиса:


    1. При инициализации приложения у сервиса есть токен. Если мы не указали его специально в провайдере, то это JokerService.
    2. При запросе сервиса в компоненте механизм DI проверяет, существует ли переданный токен.
    3. Если токена не существует, то DI кинет ошибку. В нашем случае токен существует и по нему находится JokerService.
    4. В момент создания компонента в конструктор в качестве аргумента передается экземпляр JokerService.

    Change Detection


    Мы часто слышим в качестве аргумента для использования фреймворков что-то вроде «Фреймворк все сделает за вас — быстрее и эффективнее. Вам не нужно ни о чем думать. Просто управляйте данными». Возможно, это действительно так в отношении очень простого приложения. Но если приходится работать с пользовательским вводом и постоянно оперировать данными, то знать, как работает процесс обнаружения изменений и рендеринга, просто необходимо.


    В Angular за проверку изменений отвечает механизм Change Detection. В результате различных операций — изменение значения свойства класса, завершение асинхронной операции, ответ на HTTP-запрос и так далее — запускается процесс проверки по всему дереву компонентов.


    Так как главная цель процесса — понять, как перерендерить компонент, то суть заключается в проверке данных, используемых в шаблонах. Если они разные, то шаблон помечается как «измененный» и будет перерисован.


    Zone.js


    Понять, как Angular следит за свойствами класса и синхронными операциями, довольно просто. Но как он отслеживает асинхронные? За это отвечает библиотека Zone.js, созданная одним из разработчиков Angular.


    Вот что это такое. Зона сама по себе — это «контекст выполнения», если выражаться грубо — место и состояние, в котором выполняется код. После выполнения асинхронной операции функция обратного вызова (callback) выполняется в той же зоне, где была зарегистрирована. Так Angular узнает, в каком месте произошло изменение и что следует проверить.


    Zone.js заменяет своими реализациями практически все нативные асинхронные функции и методы. Поэтому она может отследить момент, когда будет вызван callback асинхронной функции. То есть Zone сообщает Angular, когда и где нужно запустить процесс проверки изменений.


    Стратегии обнаружения изменений


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


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


    Всего на выбор два варианта:


    • Default — как можно догадаться из названия, это стратегия по умолчанию, когда на каждое действие запускается CD.
    • OnPush — стратегия, при которой CD запускается лишь в нескольких случаях:
      • если изменилось значение @Input();
      • если произошло событие внутри компонента или его потомков;
      • если проверка была запущена вручную;
      • если в Async Pipe приходит новое событие.

    Опираясь на свой собственный опыт разработки на Angular, а также на опыт моих коллег, могу сказать наверняка, что лучше всегда указывать стратегию OnPush, за исключением случаев, когда default действительно необходим. Это даст вам несколько преимуществ:


    • Четкое понимание, как работает процесс CD.
    • Аккуратная работа с @Input() свойствами.
    • Прирост производительности.

    Работа с @Input()


    Как и в других популярных фреймворках, в Angular используется нисходящий поток данных. Компонент принимает входные параметры, которые помечаются декоратором @Input(). Рассмотрим на примере:


    interface IJoke {
        author: string;
        text: string;
    }
    
    @Component({
        selector: 'joke',
        template: './joke.template.html',
    })
    export class JokeComponent {
        @Input() joke: IJoke;
    }

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


    setAuthorNameOnly() {
        const name = this.joke.author.split(' ')[0];
    
        this.joke.author = name;
    }

    Сразу отмечу, что это плохой пример, но он явно показывает, что может произойти. Чтобы защититься от таких ошибок, нужно делать входные параметры доступными только для чтения. Благодаря этому у вас будет понимание, как корректно работать с данными и вызывать CD. Исходя из этого лучший вариант написания класса будет выглядеть примерно так:


    @Component({
        selector: 'joke',
        template: './joke.template.html',
        changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class JokeComponent {
        @Input() readonly joke: IJoke;
        @Output() updateName = new EventEmitter<string>();
    
        setAuthorNameOnly() {
            const name = this.joke.author.split(' ')[0];
    
            this.updateName.emit(name);
        }
    }

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


    RxJS


    Конечно, я могу ошибаться, но складывается ощущение, что ReactiveX и реактивное программирование в целом — это новый тренд. Angular поддался этому тренду (а может, и создал его) и использует RxJS по умолчанию. Базовая логика всего фреймворка работает на данной библиотеке, поэтому очень важно понимать принципы реактивного программирования.


    Но что такое RxJS? Он объединяет три идеи, которые я раскрою довольно простым языком с некоторыми упущениями:


    • Паттерн «Наблюдатель» — сущность, которая производит события, и есть слушатель, который получает информацию об этих событиях.
    • Паттерн «Итератор» — позволяет получить последовательный доступ к элементам объекта, не раскрывая его внутренней структуры.
    • Функциональное программирование с коллекциями — паттерн, при котором логика бьется на маленькие и очень простые составные части, каждая из которых решает только одну задачу.

    Объединение этих паттернов позволяет нам очень просто описывать сложные с первого взгляда алгоритмы, например:


    private loadUnreadJokes() {
        this.showLoader(); // Ставим лоадер
    
        fromEvent(document, 'load')
            .pipe(
                switchMap(
                    () =>
                        this.http
                            .get('/api/v1/jokes') // Запрашиваем шутки
                            .pipe(map((jokes: any[]) => jokes.filter(joke => joke.unread))), // Фильтруем непрочитанные
                ),
            )
            .subscribe(
                (jokes: any[]) => (this.jokes = jokes), // Ставим шутки
                error => {
                    /* Обработка ошибки */
                },
                () => this.hideLoader(), // Скрываем лоадер вне зависимости от результата
            );
    }

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


    Observable


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


    const observable = [];
    let counter = 0;
    
    const intervalId = setInterval(() => {
        observable.push(counter++);
    }, 1000);
    
    setTimeout(() => {
        clearInterval(intervalId);
    }, 6000);

    Мы будем считать актуальным последнее значение в массиве. Каждую секунду в массив будет добавляться число. Как мы можем узнать в другом месте приложения, что в массив был добавлен элемент? В обычной ситуации мы вызывали бы какой-нибудь callback и по нему обновляли значение массива, а затем просто брали бы последний элемент.


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


    document.addEventListener('click', event => {});

    Вы можете поставить много EventListener'ов во всем приложении, и они все будут отрабатывать, если вы, конечно, не позаботились об обратном специально.


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


    Теперь давайте посмотрим на реальный пример:


    export class JokesListComponent implements OnInit {
        jokes$: Observable<IJoke>;
        authors$ = new Subject<string[]>();
        unread$ = new Subject<number>();
    
        constructor(private jokerService: JokerService) {}
    
        ngOnInit() {
            // Обратите внимание, я не использую subscribe() в этом месте
            this.jokes$ = this.jokerService.getJokes();
    
            this.jokes$.subscribe(jokes => {
                this.authors$.next(jokes.map(joke => joke.author));
                this.unread$.next(jokes.filter(joke => joke.unread).length);
            });
        }
    }

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


    TestBed


    Рано или поздно разработчик понимает, что если проект — не MVP, то нужно писать тесты. И чем больше тестов будет написано, чем понятнее и подробнее у них описание, тем проще, быстрее и надежнее вносить изменения и реализовывать новый функционал.


    Вероятно, в Angular это предвидели и дали нам мощный инструмент для тестирования. Многие разработчики сначала пытаются осилить какую-то технологию «с разбега», не вдаваясь в документацию. Так же поступал и я, отчего довольно поздно осознал все возможности тестирования, доступные «из коробки».


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


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


    @Component({
        selector: 'app-joker',
        template: '<some-dependency></some-dependency>',
        styleUrls: ['./joker.component.less'],
    })
    export class JokerComponent {
        constructor(
            private jokesService: JokesService,
            @Inject(PARTY_TOKEN) private partyService: PartyService,
            @Optional() private sleepService: SleepService,
        ) {}
    
        makeNewFriend(): IFriend {
            if (this.sleepService && this.sleepService.isSleeping) {
                this.sleepService.wakeUp();
            }
    
            const joke = this.jokesService.generateNewJoke();
    
            this.partyService.goToParty('Pacha');
            this.partyService.toSay(joke.text);
    
            const laughingPeople = this.partyService.getPeopleByReaction('laughing');
            const girl = laughingPeople.find(human => human.sex === 'female');
            const friend = this.partyService.makeFriend(girl);
    
            return friend;
        }
    }

    Итак, в текущем примере есть три сервиса. Один импортится обычным способом, один — по токену и еще один сервис опционален. Как мы сконфигурируем тестовый модуль? Я покажу сразу готовый вид:


    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [SomeDependencyModule],
            declarations: [JokerComponent], // Самое главное, что необходимо указать
            providers: [{provide: PARTY_TOKEN, useClass: PartyService}],
        }).compileComponents();
    
        fixture = TestBed.createComponent(JokerComponent);
        component = fixture.componentInstance;
        fixture.detectChanges(); // Необходимо только в случае, если вы проверяете верстку
    }));

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


    Можно избегать лишних зависимостей


    Приложение на Angular состоит из модулей, которые могут включать в себя другие модули, сервисы, директивы и прочее. В тесте нам необходимо, по сути, воссоздать работу модуля. Если в нашем примере мы используем в шаблоне <some-dependency></some-dependency>, это значит, что мы должны импортировать SomeDependencyModule и в тест. А если там есть свои зависимости? Значит, и их тоже нужно импортировать.
    Если приложение сложное, таких зависимостей будет масса. Импорт всех зависимостей приведет к тому, что в каждом тесте будет находиться вообще все приложение и будут вызываться все методы. Наверное, нам такое не подходит.


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


    TestBed.configureTestingModule({
        declarations: [JokerComponent],
        providers: [{provide: PARTY_TOKEN, useClass: PartyService}],
    })
        .overrideTemplate(JokerComponent, '') // Шаблон теперь пустой, без зависимостей
        .compileComponents();

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


    Можно мокировать все зависимости из конструктора


    Мы уже ознакомились с Injection Token, поэтому предлагаю сразу перейти к делу. В примере выше я уже запровайдил в тест сервис по токену. Если вы пишете не интеграционный тест, то смысла вызывать методы реального сервиса нет, просто сделайте мок.


    Для этого можно использовать специальные библиотеки вроде ts-mockito, которые существенно облегчат вам жизнь, но это не обязательно. Angular предоставляет массу возможностей «из коробки».


    // Создаем моковый сервис
    export class MockPartyService extends PartyService {
        meetFriend(): IFriend {
            return {} as IFriend;
        }
    
        goToParty() {}
    
        toSay(some: string) {
            console.log(some);
        }
    }
    
    // ...
    
    TestBed.configureTestingModule({
        declarations: [JokerComponent, MockComponent],
        providers: [{provide: PARTY_TOKEN, useClass: MockPartyService}], // Просто провайдим его
    }).compileComponents();

    Вот и все. Так же можно поступить с любой зависимостью из конструктора.


    Множество кейсов


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


    • Как будет работать логика при наличии и отсутствии опционального сервиса.
    • Если сервис импортируется по токену — следовательно, могут быть использованы разные его версии. Если тест интеграционный — нужно учесть все варианты.

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


    Итог


    Мы ознакомились с некоторыми базовыми механизмами Angular, знаний о которых мне не хватало в самом начале моего пути. Чтобы подробно раскрыть каждый механизм, нужна отдельная статья, которые есть в том числе и на «Хабре».


    В отличие от других современных фреймворков, Angular часто предлагает уже готовые способы реализации какого-либо механизма. Это могут быть HTTP-запросы, роутинг, lazy-loading и прочее. Поэтому я призываю всех прочитать или хотя бы пробежать глазами официальную документацию Angular.

    Tinkoff.ru
    it’s Tinkoff — просто о сложном

    Комментарии 68

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

          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.
      Можете объяснить мне это?

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

          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()
            0
            Немного неправильный пример, вы забыли про операторы. Ещё вместо `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) {}
            }
            
              0

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

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

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

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

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

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

                  0

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

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

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


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

                      0

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

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

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

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

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

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

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

                          0

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

                            +1

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

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

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

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

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



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


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

                          0

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

                          +1
                          Всего 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(); // Скрываем лоадер вне зависимости от результата
                            });
                          }
                          

                            0

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


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

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

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

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


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

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

                                    Нее, это так не работает. Любая ошибка завершает поток, 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)),
                                        )
                                      0

                                      Спасибо, согласен с вами.

                                        0
                                        не забудьте что empty так же немедленно завершит поток.
                                        Не в «одноразовых» потоках он не всегда уместен.
                                          –1
                                          Да согласен, еще один минус rxjs :)
                                0

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

                                  0

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

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

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


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


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

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

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

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


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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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


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

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

                                          Спасибо за статью, я бы еще немного зарефакторил метод 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())
                                          )
                                            0
                                            1. Про вложенность ответил в комментарии выше, но я согласен, что так читается удобнее.
                                            2. Спасибо, действительно, так даже читаться будет проще.
                                            +1
                                            если изменилось значение Input();
                                            если произошло событие внутри компонента или его потомков;
                                            если проверка была запущена вручную.
                                            CD также дернется когда новое значение пролетает через AsyncPipe используемый в шаблоне компонента. Часто бывает удобно, особенно когда используется реактивный стор (NgRx или самодельный на BehaviourSubject).
                                              0

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

                                                0
                                                Событие то происходит не в самом компоненте, компонент только транслирует событие/нотификацию/порцию-данных-в-стриме в шаблон. По моему мнению async pipe нужно указывать отдельным пунктом при перечислении тригерров CD при использовании onPush стратегии тк для новичков это не очевидно.
                                                0
                                                асинк-пайп просто сам вызывает местный markForCheck
                                                github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L143
                                                  0
                                                  И еще сам отписывается. Это все понятно, просто по моему мнению async pipe нужно указывать отдельным пунктом тк для новичков это не очевидно.
                                                    0
                                                    да, этот коммент предназначался скорее автору топика. Он уже кажется немного перепутал перехват дом-событий и подписки, это следствие ангулярной магии, которая нигде особо не объясняется.
                                                      0

                                                      Хорошо, согласен. Обновил)

                                                0

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

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

                                                Самое читаемое