- PVSM.RU - https://www.pvsm.ru -
С помощью Angular можно сделать всё что угодно. Или почти всё. Но иногда это коварное «почти» приводит к тому, что разработчик губит время, создавая обходные решения, или пытаясь понять, почему что-то происходит, или почему что-то не работает так, как ожидается.
Автор статьи, перевод которой мы сегодня публикуем, говорит, что хочет поделиться советами, которые помогут Angular-разработчикам сэкономить немного времени. Он собирается рассказать о подводных камнях Angular, с которыми ему (и не только ему) довелось встретиться.
Итак, вы обнаружили симпатичную директиву Angular сторонней разработки и решили использовать её со стандартными элементами в шаблоне Angular. Замечательно! Попробуем это сделать:
<span awesomeTooltip="'Tooltip text'"> </span>
Вы запускаете приложение… И ничего не происходит. Вы, как любой нормальный опытный программист, заглядываете в консоль инструментов разработчика Chrome. И ничего там не видите. Директива не работает и Angular хранит молчание.
Потом какая-нибудь светлая голова из вашей команды решает поместить директиву в квадратные скобки.
<span [awesomeTooltip]="'Tooltip text'"> </span>
После этого, потеряв немного времени, мы видим в консоли следующее.
Вот в чём дело: мы просто забыли импортировать модуль с директивой
Сейчас причина проблемы совершенно очевидна: мы просто забыли импортировать модуль директивы в модуль приложения Angular.
Отсюда выводим важное правило: никогда не используйте директивы без квадратных скобок.
Поэкспериментировать с директивами можно здесь [2].
Предположим, вы создали ссылку на элемент для ввода текста, описанный в шаблоне Angular.
<input type="text" name="fname" #inputTag>
Вы собираетесь, с помощью функции RxJS fromEvent [3], создать поток, в который будет попадать то, что вводится в поле. Для этого вам понадобится ссылка на поле ввода, которую можно получить с помощью декоратора Angular ViewChild
:
class SomeComponent implements AfterViewInit {
@ViewChild('inputTag') inputTag: ElementRef;
ngAfterViewInit(){
const input$ = fromEvent(this.inputTag.nativeElement, 'keyUp')
}
...
}
Здесь мы, с помощью функции RxJS fromEvent
, создаём поток, в который будут попадать данные, вводимые в поле.
Испытаем этот код.
Ошибка
Что случилось?
Собственно говоря, здесь применимо следующее правило: если ViewChild
возвращает undefined
— поищите в шаблоне *ngIf
.
<div *ngIf="someCondition">
<input type="text" name="fname" #inputTag>
</div>
Вот он — виновник проблемы.
Кроме того, проверьте шаблон на наличие в нём других структурных директив или ng-template
выше проблемного элемента.
Рассмотрим возможные варианты решения этой проблемы.
Можно просто скрыть элемент шаблона в том случае, если он вам не нужен. В этом случае элемент всегда будет продолжать существовать и ViewChild
сможет вернуть ссылку на него в хуке ngAfterViewInit
.
<div [hidden]="!someCondition">
<input type="text" name="fname" #inputTag>
</div>
Ещё один способ решения этой проблемы заключается в использовании сеттеров.
class SomeComponent {
@ViewChild('inputTag') set inputTag(input: ElementRef|null) {
if(!input) return;
this.doSomething(input);
}
doSomething(input) {
const input$ = keysfromEvent(input.nativeElement, 'keyup');
...
}
}
Тут, как только Angular назначает свойству inputTag
определённое значение, мы создаём поток из данных, введённых в поле ввода.
Вот пара полезных ресурсов, имеющих отношение к этой проблеме:
ViewChild
в Angular 8 могут быть статическими и динамическими.
Предположим, у вас имеется какая-нибудь интересная пользовательская директива для организации прокручиваемых списков. Вы собираетесь применить её к списку, который создан с помощью директивы Angular *ngFor
.
<div *ngFor="let item of itemsList; let i = index;"
[customScroll]
>
<p *ngFor="let item of items" class="list-item">{{item}}</p>
</div>
Обычно в подобных случаях при обновлении списка нужно вызвать нечто вроде scrollDirective.update
для настройки поведения скроллинга с учётом изменений, произошедших в списке.
Может показаться, что это можно сделать с помощью хука ngOnChanges
:
class SomeComponent implements OnChanges {
@Input() itemsList = [];
@ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;
ngOnChanges(changes) {
if (changes.itemsList) {
this.scroll.update();
}
}
...
}
Правда, тут мы встречаемся с проблемой. Хук вызывается до вывода обновлённого списка браузером. В результате пересчёт параметров директивы для организации прокрутки списка выполняется неправильно.
Как выполнить вызов сразу после того, как *ngFor
завершит работу?
Сделать это можно, выполнив следующие 3 простых шага:
Поместим ссылки на элементы туда, где применяется *ngFor
(#listItems
).
<div [customScroll]>
<p *ngFor="let item of items" #listItems>{{item}}</p>
</div>
Получим список этих элементов с помощью декоратора Angular ViewChildren
. Он возвращает сущность типа QueryList
.
Класс QueryList
имеет свойство changes [6], предназначенное только для чтения, которое выдаёт события каждый раз, когда меняется список.
class SomeComponent implements AfterViewInit {
@Input() itemsList = [];
@ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;
@ViewChildren('listItems') listItems: QueryList<any>;
private sub: Subscription;
ngAfterViewInit() {
this.sub = this.listItems.changes.subscribe(() => this.scroll.update())
}
...
}
Теперь проблема решена. Здесь [7] можно поэкспериментировать с соответствующим примером.
Понять суть этой проблемы нам поможет следующий код.
// app-routing.module.ts
const routes: Routes = [
{path: '', redirectTo: '/home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
];
@NgModule({
imports: [RouterModule.forRoot(routes)], // Фрагмент #1
exports: [RouterModule]
})
export class AppRoutingModule { }
//app.module.ts
@NgModule({
...
bootstrap: [AppComponent] // Фрагмент #2
})
export class AppModule { }
// app.component.html
<router-outlet></router-outlet> // Фрагмент #3
// app.component.ts
export class AppComponent implements OnInit {
title = 'QueryTest';
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.queryParams
.subscribe(params => {
console.log('saveToken', params); // Фрагмент #4
});
}
}
К некоторым фрагментам этого кода сделаны комментарии вида Фрагмент #x
. Рассмотрим их:
RouterModule
. Маршруты настроены так, что если в URL не предоставлен маршрут, мы перенаправляем пользователя на страницу /home
.AppComponent
.AppComponent
использует <router-outlet>
для вывода соответствующих компонентов маршрута.queryParams
для маршрута из URLПредположим, что нам достался такой URL:
https://localhost:4400/home?accessToken=someTokenSequence
В таком случае queryParams
будет выглядеть так:
{accessToken: ‘someTokenSequence’}
Посмотрим на работу всего этого в браузере.
Тестирование приложения, в котором реализована система маршрутизации
Тут у вас может появиться вопрос о сути проблемы. Параметры мы получили, всё работает как ожидается…
Присмотритесь к приведённой выше копии экрана браузера, и к тому, что выводится в консоль. Тут можно заметить, что объект queryParams
выдаётся дважды. Первый объект оказывается пустым, он выдаётся в ходе процесса инициализации маршрутизатора Angular. Только после этого мы получаем объект, в котором содержатся параметры запроса (в нашем случае — {accessToken: ‘someTokenSequence’}
).
Проблема заключается в том, что если в URL не будет никаких параметров запроса, то маршрутизатор не выдаст ничего. То есть — после выдачи первого пустого объекта второй объект, тоже пустой, который мог бы указывать на отсутствие параметров, выдан не будет.
Второй объект при выполнении запроса без параметров не выдаётся
В результате оказывается, что если код ожидает второго объекта, из которого он может получить данные запроса, то он не будет запущен в том случае, если в URL не было параметров запроса.
Как решить эту проблему? Здесь нам может помочь RxJs. Мы создадим на основе ActivatedRoute.queryParams
два наблюдаемых объекта. Как обычно — рассмотрим пошаговое решение проблемы.
Первый наблюдаемый объект, paramsInUrl$
, будет выдавать данные в том случае, если значение queryParams
не является пустым:
export class AppComponent implements OnInit {
constructor(private route: ActivatedRoute,
private locationService: Location) {
}
ngOnInit() {
// Отфильтровываем первое пустое значение
// И повторно выдаём значение с параметрами
const paramsInUrl$ = this.route.queryParams.pipe(
filter(params => Object.keys(params).length > 0)
);
...
}
}
Второй наблюдаемый объект, noParamsInUrl$
, будет выдавать пустое значение только в том случае, если в URL не было обнаружено параметров запроса:
export class AppComponent implements OnInit {
title = 'QueryTest';
constructor(private route: ActivatedRoute,
private locationService: Location) {
}
ngOnInit() {
...
// Выдаёт пустой объект, но только в том случае, если в URL
// не было обнаружено параметров запроса
const noParamsInUrl$ = this.route.queryParams.pipe(
filter(() => !this.locationService.path().includes('?')),
map(() => ({}))
);
...
}
}
Теперь скомбинируем наблюдаемые объекты с помощью функции RxJS merge [8]:
export class AppComponent implements OnInit {
title = 'QueryTest';
constructor(private route: ActivatedRoute,
private locationService: Location) {
}
ngOnInit() {
// Отфильтровываем первое пустое значение
// И повторно выдаём значение с параметрами
const paramsInUrl$ = this.route.queryParams.pipe(
filter(params => Object.keys(params).length > 0)
);
// Выдаёт пустой объект, но только в том случае, если в URL
// не было обнаружено параметров запроса
const noParamsInUrl$ = this.route.queryParams.pipe(
filter(() => !this.locationService.path().includes('?')),
map(() => ({}))
);
const params$ = merge(paramsInUrl$, noParamsInUrl$);
params$.subscribe(params => {
console.log('saveToken', params);
});
}
}
Теперь наблюдаемый объект param$
выдаёт значение лишь один раз — независимо от того, содержится ли что-нибудь в queryParams
(выдаётся объект с параметрами запроса) или нет (выдаётся пустой объект).
Поэкспериментировать с этим кодом можно здесь [9].
Предположим, у вас имеется компонент, который выводит некие отформатированные данные:
// home.component.html
<div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">
<div *ngFor="let item of items">
<span>{{formatItem(item)}}</span>
</div>
</div>
{{mouseCoordinates | json}}
// home.component.ts
export class HomeComponent {
items = [1, 2, 3, 4, 5, 6];
mouseCoordinates = {};
formatItem(item) {
// Имитация тяжёлых вычислений
const t = Array.apply(null, Array(5)).map(() => 1);
console.log('formatItem');
return item + '%';
}
}
Этот компонент решает две задачи:
formatItem
.Вы не ожидаете того, что у этого компонента будут какие-то проблемы с производительностью. Поэтому запускаете тест производительности только для того, чтобы соблюсти все формальности. Однако в ходе этого теста проявляются некие странности.
Много вызовов formatItem и довольно большая нагрузка на процессор
В чём же дело? А дело в том, что когда Angular перерисовывает шаблон, он вызывает и все функции из шаблона (в нашем случае — функцию formatItem
). В результате, если в функциях шаблона выполняются какие-нибудь тяжёлые вычисления, это создаёт нагрузку на процессор и влияет на то, как пользователи будут воспринимать соответствующую страницу.
Как это исправить? Достаточно выполнить вычисления, выполняемые в formatItem
, заранее, и вывести на страницу уже готовые данные.
// home.component.html
<div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">
<div *ngFor="let item of displayedItems">
<span>{{item}}</span>
</div>
</div>
{{mouseCoordinates | json}}
// home.component.ts
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.sass']
})
export class HomeComponent implements OnInit {
items = [1, 2, 3, 4, 5, 6];
displayedItems = [];
mouseCoordinates = {};
ngOnInit() {
this.displayedItems = this.items.map((item) => this.formatItem(item));
}
formatItem(item) {
console.log('formatItem');
const t = Array.apply(null, Array(5)).map(() => 1);
return item + '%';
}
}
Теперь тест производительности выглядит гораздо приличнее.
Всего 6 вызовов formatItem и низкая нагрузка на процессор
Теперь приложение работает гораздо лучше. Но у применённого здесь решения есть некоторые особенности, не всегда приятные:
mousemove
всё ещё приводит к запуску проверки изменений. Но, так как нам нужны координаты мыши, избавиться от этого мы не можем.mousemove
должны лишь выполняться некие вычисления (которые не влияют на то, что выводится на странице), тогда, чтобы ускорить приложение, можно поступить следующим образом:
NgZone.runOutsideOfAngular
. Это позволяет предотвратить запуск проверки изменений при возникновении события mousemove
(это повлияет исключительно на данный обработчик).* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // отключить патч для указанных событий
Если вас интересуют вопросы повышения производительности Angular-приложений — вот [11], вот [12], вот [13] и вот [14] — полезные материалы об этом.
Сейчас, когда вы прочли эту статью, в вашем арсенале Angular-разработчика должно появиться 5 новых инструментов, с помощью которых вы сможете решать некоторые распространённые проблемы. Надеемся, советы, которые вы здесь нашли, помогут вам сэкономить немного времени.
Уважаемые читатели! Знаете ли вы о чём-то таком, что, при разработке Angular-приложений, помогает экономить время?
Автор: ru_vds
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/323573
Ссылки в тексте:
[1] Image: https://habr.com/ru/company/ruvds/blog/459304/
[2] здесь: https://stackblitz.com/edit/angular-f4wesb?file=src/app/app.component.html
[3] fromEvent: https://rxjs.dev/api/index/function/fromEvent
[4] Здесь: https://habr.com/ru/company/ruvds/blog/453584/
[5] этот: https://www.udemy.com/hands-on-rxjs-for-web-development/
[6] changes: https://github.com/angular/angular/blob/master/packages/core/src/linker/query_list.ts#L48
[7] Здесь: https://stackblitz.com/edit/ngx-perfect-scrollbar-ngfor-update?file=src%2Fapp%2Fapp.component.ts
[8] merge: https://rxjs.dev/api/index/function/merge
[9] здесь: https://stackblitz.com/edit/angular-8-communicating-between-components-ruc8mf?file=app%2Fapp.component.ts
[10] polyfills.ts: https://github.com/angular/angular-cli/blob/888bb27815097e03d82c85aef1a3547ea7769b5c/packages/schematics/angular/application/files/src/polyfills.ts.template#L46
[11] вот: https://blog.angularindepth.com/optimize-angular-bundle-size-in-4-steps-4a3b3737bf45
[12] вот: https://blog.angularindepth.com/improve-performance-with-web-workers-497931fdef1b
[13] вот: https://blog.angularindepth.com/how-to-fix-the-most-common-angular-performance-problems-like-a-doc-770998e05814
[14] вот: https://medium.com/@krzysztof.grzybek89/how-runoutsideangular-might-reduce-change-detection-calls-in-your-app-6b4dab6e374d
[15] Источник: https://habr.com/ru/post/459304/?utm_source=habrahabr&utm_medium=rss&utm_campaign=459304
Нажмите здесь для печати.