Angular может все — ну или почти все. Но иногда это «почти» заставляет вас тратить время на написание обходных решений или на попытки понять, почему что-то происходит или не работает, как нужно. Я хочу сэкономить вам время и рассказать об этих ловушках Angular, основываясь на собственном опыте. Начнем.
#1 Пользовательская директива не работает
Итак, вы нашли очень хорошую директиву Angular стороннего производителя и собираетесь использовать ее для нативных элементов в шаблоне Angular. Отлично, давайте сделаем это.

Запускаем приложение и… оно не работает. Как опытный разработчик вы проверяете консоль Chrome. И ничего не видите 🙂
Тогда какая-то светлая голова в вашей команде догадывается заключить директиву в квадратные скобки:
Теперь, потратив время, мы находим причину:

Проблема очевидна: мы просто забыли импортировать модуль директивы в модуль приложения Angular.
Итак, важное правило: никогда не используйте директивы без квадратных скобок.
Проверить, как это работает, можно в песочнице Stackblitz.
#2 ViewChild возвращает undefined
Вы помещаете ссылку на элемент input
в шаблон Angular.

И хотите создать поток с обновлениями ввода текста с помощью RxJS функции fromEvent
. Вам нужно ввести нативный элемент ref
, который можно получить с помощью декоратора Angular ViewChild
:

fromEvent
Давайте запустим код:

ЧТО?
Здесь главное правило: если ViewChild
возвращает undefined
, ищите *ngIf
в шаблоне.

Кроме того, проверьте, есть ли другие структурные директивы или ng-шаблоны над этим элементом.
Возможные решения:
- Просто спрячьте элемент в шаблоне, когда он вам не нужен. В этом случае элемент всегда существует, и
ViewChild
может вернуть ссылку на него в хук-функцииngAfterViewInit
.

2. Другая возможность — использовать сеттер:
Angular единожды присваивает inputTag
заданное значение, тогда мы создаем поток из введенных данных элемента.
#3 Запускаем код после обновления *ngFor
Скажем, у вас есть хорошая пользовательская директива прокрутки. Вы хотите применить ее в списке, сгенерированном *ngFor
.
Если список обновился, мы обычно запускаем метод scrollDirective.update
, чтобы пересчитать математику прокрутки, так как общая высота элементов тоже изменилась. Можно сделать это внутри хук-функции ngOnChanges
:
Но… метод вызывается до того, как список новых элементов отображается в браузере. Значит, пересчет директивы прокрутки будет неверным ?.
Но как вызвать метод сразу после того, как *ngFor
закончит свою работу? Это можно сделать в 3 простых шага:
- Поместите ссылки на элементы, к которым применяется
*ngFor
(#listItems
).

- С помощью декоратора
ViewChildren
получите список этих элементов. Он вернет объект типаQueryList
. - Класс
QueryList
предоставляет свойство только для чтенияchanges
. Оно оповещает каждый раз, когда изменяется список.
Проблема решена. С примерами можно поиграть в песочнице.
#4 Проблема с ActivatedRoute.queryParam
Чтобы понять следующую проблему, подробно рассмотрим этот код:
- Мы определяем маршруты и добавляем
RouterModule
к основному модулю приложения. Маршруты сконфигурированы так: если в URL не указан маршрут, мы перенаправляем использование на страницу /home. AppComponent
— корневой.AppComponent
использует<router-outlet>
для отображения соответствующих компонентов маршрута.- Основная часть: мы хотим получить
queryParams
маршрута из URL.
Например:
https://localhost:4400/home?accessToken=someTokenSequence
Тогда у запроса один параметр: {accessToken: ‘someTokenSequence’}
.

Вы можете спросить — в чем проблема? Параметры получены — бинго!
Хорошо, если вы внимательно посмотрите на скриншот, вы можете заметить, что queryParams
отображается дважды. Первый эмит пустого объекта происходит как часть процесса инициализации Angular Router. И только после этого мы получаем объект с актуальными queryParams
({accessToken: ‘someTokenSequence’}
в примере).
Проблема вот в чем: если в URL не указаны параметры запроса, то маршрутизатор ничего не эмитит: нет второго пустого объекта, который сообщит, что параметров нет.

Значит, код ждет повторного оповещения, чтобы получить актуальные данные (пустые или нет). Он никогда не запустится, если в URL нет параметров. Как решить эту проблему? Здесь нам поможет RxJS. Создадим два Observable
из ActivatedRoute.queryParams
:
- Первый
Observable
paramsInUrl$
сработает, если значениеqueryParams
не пустое:
- Второй
Observable
noParamsInUrl$
эмитит пустой объект, если в URL нет параметров:
Теперь скомбинируем их с RxJS функцией merge
:
Составная Observable
params$
испускает значение только один раз, независимо от того, присутствует ли queryParams
(выдан пустой объект) или нет (выдан объект со значениями queryParams
).
Проверить, как работает код, можно здесь.
#5 Низкий FPS страницы
Есть компонент, отображающий некоторые отформатированные значения, например:
Этот компонент делает две вещи:
- Отображает массив элементов (предполагается, что один раз). Также, вызывая метод
formatItem
, он форматирует отображаемый вывод. - Отображает координаты мыши (значение определенно будет обновляться часто).
Вы не ожидаете большого влияния на производительность и запускаете тест — просто чтобы соблюсти формальности. Тест показывает что-то странное:

Но почему?
Потому что каждый раз, перерисовывая шаблон, Angular вызывает все его функции (в нашем случае это formatItem
). Следовательно, выполнение функцией трудоемких вычислений в шаблоне влияет на загрузку ЦП и впечатления пользователя.
Как это исправить? Просто сделать предварительные вычисления formatItem
и отобразить рассчитанное значение.
Теперь результаты теста выглядят приемлемо.

Приложение работает лучше, но у этого решения есть свои минусы:
- В шаблоне отображаются координаты мыши. А значит, событие
mousemove
все еще провоцирует запуск обнаружения изменений. Но это неизбежно, раз нам нужны эти координаты. - Если обработчик
mousemove
должен выполнять только вычисления (без отображения результата), вот что ускорит работу приложения:
1. Можно использовать NgZone.runOutsideOfAngular
внутри функции обработчика, чтобы предотвратить обнаружение изменений в mousemove
(это влияет только на конкретный обработчик).
2. Можно предотвратить исправление zone.js
для некоторых событий, добавив эту строку в polyfill.ts
. Это повлияет на все приложение.
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // отключает изменение указанного eventNames
Подробнее на эту тему на русском языке:
Перевод статьи Alexander Poshtaruk: Beware! Angular can steal your time