Angular

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

#1 Пользовательская директива не работает

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

Не сработало, Angular молчит.

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

Тогда какая-то светлая голова в вашей команде догадывается заключить директиву в квадратные скобки:

Теперь, потратив время, мы находим причину:

Ха! Мы просто забыли импортировать модуль вместе с директивой.

Проблема очевидна: мы просто забыли импортировать модуль директивы в модуль приложения Angular.

Итак, важное правило: никогда не используйте директивы без квадратных скобок.

Проверить, как это работает, можно в песочнице Stackblitz.

#2 ViewChild возвращает undefined

Вы помещаете ссылку на элемент inputв шаблон Angular.

Ссылка #inputTag

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

Создание потока из изменений данных элемента ввода с помощью RxJS функции fromEvent

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

Хорошая картинка 🙂

ЧТО?

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

Вот он, наш виновник *ngIf

Кроме того, проверьте, есть ли другие структурные директивы или ng-шаблоны над этим элементом.

Возможные решения:

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

2. Другая возможность  —  использовать сеттер:

Angular единожды присваивает inputTagзаданное значение, тогда мы создаем поток из введенных данных элемента.

#3 Запускаем код после обновления *ngFor

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

Если список обновился, мы обычно запускаем метод scrollDirective.update, чтобы пересчитать математику прокрутки, так как общая высота элементов тоже изменилась. Можно сделать это внутри хук-функции ngOnChanges:

Но… метод вызывается до того, как список новых элементов отображается в браузере. Значит, пересчет директивы прокрутки будет неверным 😒.

Но как вызвать метод сразу после того, как *ngForзакончит свою работу? Это можно сделать в 3 простых шага:

  •  Поместите ссылки на элементы, к которым применяется *ngFor (#listItems).
  • С помощью декоратора ViewChildrenполучите список этих элементов. Он вернет объект типа QueryList.
  • Класс QueryList предоставляет свойство только для чтения changes. Оно оповещает каждый раз, когда изменяется список.

Проблема решена. С примерами можно поиграть в песочнице.

#4 Проблема с ActivatedRoute.queryParam

Чтобы понять следующую проблему, подробно рассмотрим этот код:

  1. Мы определяем маршруты и добавляем RouterModule к основному модулю приложения. Маршруты сконфигурированы так: если в URL не указан маршрут, мы перенаправляем использование на страницу /home.
  2. AppComponent  —  корневой.
  3. AppComponent использует <router-outlet> для отображения соответствующих компонентов маршрута. 
  4. Основная часть: мы хотим получить queryParams маршрута из URL. 

Например:

https://localhost:4400/home?accessToken=someTokenSequence

Тогда у запроса один параметр: {accessToken: ‘someTokenSequence’}.

Тестовое приложение с маршрутом

Вы можете спросить  —  в чем проблема? Параметры получены  —  бинго!

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

Проблема вот в чем: если в URL не указаны параметры запроса, то маршрутизатор ничего не эмитит: нет второго пустого объекта, который сообщит, что параметров нет.

Нет повторного оповещения из ActivatedRoute.queryParams

Значит, код ждет повторного оповещения, чтобы получить актуальные данные (пустые или нет). Он никогда не запустится, если в URL нет параметров. Как решить эту проблему? Здесь нам поможет RxJS. Создадим два Observable из ActivatedRoute.queryParams:

  • Первый Observable paramsInUrl$сработает, если значение queryParamsне пустое:
  •  Второй Observable noParamsInUrl$эмитит пустой объект, если в URL нет параметров:

Теперь скомбинируем их с RxJS функцией merge:

Составная Observable params$ испускает значение только один раз, независимо от того, присутствует ли queryParams (выдан пустой объект) или нет (выдан объект со значениями queryParams).

Проверить, как работает код, можно здесь.

#5 Низкий FPS страницы

Есть компонент, отображающий некоторые отформатированные значения, например:

Этот компонент делает две вещи:

  1. Отображает массив элементов (предполагается, что один раз). Также, вызывая метод formatItem, он форматирует отображаемый вывод.
  2. Отображает координаты мыши (значение определенно будет обновляться часто).

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

Множество вызовов formatItem и высокая загрузка ЦП.

 Но почему?

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

Как это исправить? Просто сделать предварительные вычисления formatItem и отобразить рассчитанное значение.

Теперь результаты теста выглядят приемлемо. 

Только 6 вызовов formatItem и низкая загрузка ЦП.

Приложение работает лучше, но у этого решения есть свои минусы:

  • В шаблоне отображаются координаты мыши. А значит, событие mousemove все еще провоцирует запуск обнаружения изменений. Но это неизбежно, раз нам нужны эти координаты.
  • Если обработчик mousemoveдолжен выполнять только вычисления (без отображения результата), вот что ускорит работу приложения:

1. Можно использовать NgZone.runOutsideOfAngular внутри функции обработчика, чтобы предотвратить обнаружение изменений в mousemove(это влияет только на конкретный обработчик).

2. Можно предотвратить исправление zone.js для некоторых событий, добавив эту строку в polyfill.ts. Это повлияет на все приложение.

* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // отключает изменение указанного eventNames

Подробнее на эту тему на русском языке:

  1. Оптимизация размера Angular bundle за 4 шага.
  2. Улучшите производительность с помощью веб-воркеров.

Перевод статьи Alexander Poshtaruk: Beware! Angular can steal your time