Источник: Nuances of Programming
Angular может все — ну или почти все. Но иногда это «почти» заставляет вас тратить время на написание обходных решений или на попытки понять, почему что-то происходит или не работает, как нужно. Я хочу сэкономить вам время и рассказать об этих ловушках Angular, основываясь на собственном опыте. Начнем.
1 Пользовательская директива не работает
Итак, вы нашли очень хорошую директиву Angular стороннего производителя и собираетесь использовать ее для нативных элементов в шаблоне Angular. Отлично, давайте сделаем это.
Запускаем приложение и… оно не работает. Как опытный разработчик вы проверяете консоль Chrome. И ничего не видите 🙂
Тогда какая-то светлая голова в вашей команде догадывается заключить директиву в квадратные скобки:
Теперь, потратив время, мы находим причину:
Проблема очевидна: мы просто забыли импортировать модуль директивы в модуль приложения Angular.
Итак, важное правило: никогда не используйте директивы без квадратных скобок.
Проверить, как это работает, можно в песочнице Stackblitz.
2 ViewChild возвращает undefined
Вы помещаете ссылку на элемент inputв шаблон Angular.
И хотите создать поток с обновлениями ввода текста с помощью RxJS функции fromEvent. Вам нужно ввести нативный элемент ref, который можно получить с помощью декоратора Angular ViewChild:
Давайте запустим код:
ЧТО?
Здесь главное правило: если 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
Подробнее на эту тему на русском языке:
Читайте нас в телеграмме и vk
Перевод статьи Alexander Poshtaruk: Beware! Angular can steal your time