От автора: решение проблем в приложении на Angular может быть затруднительным. Ваше приложение не работает, а в консоли отображаются какие-то загадочные красные строки. Особенно когда вы еще новичок, эти проблемы могут сильно замедлить процесс разработки. Но поверьте мне, решение проблем не должно быть таким болезненным. Я считаю, что разработка на Angular одна из самых плавных. Вам лишь нужно избегать распространенных ошибок, с которыми сталкивались почти все, кто разрабатывал на Angular приложение.
По крайней мере, я сталкивался. После этой статьи эти ошибки больше не будут вас тормозить при разработке. Ниже мы подробно разберем все 11 ошибок. Я объясню, почему это ошибка, и укажу вам верное направление с, как минимум, одним решением.
1. Импорт обязательных модулей Angular
Возможно, самая распространенная ошибка новичков – они не импортируют обязательные модули. Почему? Потому что они о них даже не знают. Конечно, не знают. Изучение фреймворка Angular занимает какое-то время. К сожалению, часто это приводит к мистическим образом не работающим приложениям. Вы можете получить следующие ошибки: Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’
Эта ошибка значит, что вы не импортировали Angular Forms Module в модуль. Unhandled Promise rejection: No provider for HttpClient!
Эта ошибка значит, что вы не импортировали HttpClient Module в свой (корневой) модуль.
Решение
Для решения этой проблемы необходимо импортировать отсутствующий модуль в свой модуль. В большинстве случаев это будет модуль AppModule в папке приложения.
1 2 3 4 5 6 7 8 9 10 |
@NgModule({ declarations: [AppComponent], imports: [ BrowserModule, FormsModule, HttpClientModule ], bootstrap: [AppComponent] }) export class AppModule {} |
Обратите внимание: импортируйте только необходимые модули! Импорт ненужных модулей существенно раздувает вес приложения. Этот совет касается не только модулей angular. Он также касается любых модулей Angular, которые вы можете использовать, в том числе и сторонние модули. Распространенные модули, которые, возможно, необходимо импортировать:
1 2 3 4 5 |
BrowserModule FormsModule // required to use ngModel directive HttpClientModule // formerly HttpModule RouterModule BrowserAnimationsModule / NoopAnimationsModule |
Для сторонних библиотек хорошей практикой считается максимальное разбиение на модули. Это уменьшит вес приложения. В Angular Material, например, необходимо импортировать только модули для используемых вами компонентов. Например:
1 2 3 4 5 6 |
MatMenuModule MatSidenavModule MatCheckboxModule MatDatepickerModule MatInputModule ... |
2. Не используйте DOM ссылки пока они не созданы (@ViewChild)
Декоратор @ViewChild сильно упрощает создание ссылок на дочерние элементы (HTML узлы или компоненты) компонента. Вам нужно лишь добавить ID ссылки в узел или компонент в вашем шаблоне. Просто вставьте # и далее имя, но без атрибутов узлов.
1 |
<div #myDiv></div> |
Теперь на этот компонент можно ссылаться из нашего компонента. Если это компонент, мы можем вызывать его публичные методы и свойства. Если это чистый HTML элемент, мы можем менять его стили, атрибуты и дочерние элементы.
Angular автоматически присваивает ссылку свойству компонента, если это свойство декорировано с помощью @ViewChild(). Не забудьте передать имя ссылки в декоратор. Например, @ViewChild(‘myDiv’).
1 2 3 4 5 6 |
import { ViewChild } from '@angular/core'; @Component({}) export class ExampleComponent { @ViewChild('myDiv') divReference; } |
Проблема
@ViewChild() – очень полезная директива. Но нужно помнить: Ссылку на элемент можно использовать только в том случае, если элемент существует! А почему его не должно быть? Есть множество причин, по которым элемент, на который вы ссылаетесь, может не существовать.
Самая частая причина – браузер не закончил его создание и не успел добавить его в DOM. Если вы пытаетесь использовать его до создания, приложение упадет. Если вы знакомы с JS, вы, возможно, уже сталкивались с такой проблемой, так как она не специфична для Angular.
Один из примеров ссылки на DOM, который еще не создан – через конструктор компонента. Еще один пример – в жизненном цикле ngOnInit.
Это работать не будет:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { ViewChild, OnInit } from '@angular/core'; @Component({}) export class ExampleComponent implements OnInit{ @ViewChild('myDiv') divReference; constructor(){ let ex = this.divReference.nativeElement; // divReference is undefined } ngOnInit(){ let ex = this.divReference.nativeElement; // divReference is undefined } } |
Решение
Как событие DOMContentLoaded или $(document).ready() колбек в jQuery, Angular имеет такой же механизм уведомления о том, что все элементы HTML созданы. Он называется хук жизненного цикла ngAfterViewInit . Этот колбек вам и нужно использовать. Он срабатывает, когда все представления компонентов и дочерние представления инициализированы. Так безопасно (почти) получать доступ к ссылке viewChild внутри колбека.
1 2 3 4 5 6 7 8 9 10 |
import { ViewChild, AfterViewInit } from '@angular/core'; @Component({}) export class ExampleComponent implements AfterViewInit { @ViewChild('myDiv') divReference; ngAfterViewInit(){ let ex = this.divReference.nativeElement; // divReference is NOT undefined } } |
Ура, все работает. Но стойте. Есть еще одна ловушка. Как я сказал ранее, получить доступ можно только к уже созданным элементам. Далее мы узнаем, что элементы с директивой *ngIf, которая возвращает false, полностью удаляются из DOM. То есть мы не можем получить к ним доступ в таком случае.
Чтобы предотвратить падение приложения, необходимо проверить ссылки на null. И кстати, совет относится не только к компонентам или Angular, но и к любому языку программирования.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { ViewChild, AfterViewInit } from '@angular/core'; @Component({}) export class ExampleComponent implements AfterViewInit { @ViewChild('myDiv') divReference; ngAfterViewInit(){ let ex; if( this.divReference ){ ex = this.divReference.nativeElement; // divReference is NOT undefined } } } |
3. Не манипулируйте DOM напрямую — Angular Universal
Прямое манипулирование DOM в Angular не только не поощряется, но и может привести к отказу приложения работать в разном ПО, отличном от браузера. Самый популярный пример — Angular Universal проект, который позволяет делать рендер приложения на сервере. Но зачем это вообще делать? Прочитайте «все о Angular universal и серверном рендере в этом пошаговом руководстве». Этот пример работать не будет
1 2 3 4 5 6 7 8 9 10 11 |
import { ViewChild, AfterViewInit } from '@angular/core'; @Component({}) export class ExampleComponent implements AfterViewInit { @ViewChild('myDiv') divReference; ngAfterViewInit(){ let ex = this.divReference.nativeElement; ex.style.color = 'red'; // does not work on the server } } |
Решение
Вместо прямого изменения элементов необходимо манипулировать ими косвенно. Angular предлагает API в форме класса Renderer2. Да, 2 означает «международный», и да, был Renderer (1). Не лучшее название, но что есть.
С помощью renderer можно делать все, что и раньше при работе с DOM. Однако при работе с renderer мы точно знаем, что наш код работает на сервере так же, как и в клиенте. Вот как решается эта проблема:
1. Получите объект Renderer2, запросив его через Dependency Injection в конструкторе
1 2 3 4 5 6 7 8 9 |
import { ViewChild, Renderer2 } from '@angular/core'; @Component({}) export class ExampleComponent{ @ViewChild('myDiv') divReference; constructor(private renderer: Renderer2){ } } |
2. Манипулируйте DOM косвенно с помощью renderer. Проверьте, чтобы ссылки на элементы существовали.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { ViewChild, Renderer2, AfterViewInit } from '@angular/core'; @Component({}) export class ExampleComponent implements AfterViewInit{ @ViewChild('myDiv') divReference; constructor(private renderer: Renderer2){ } ngAfterViewInit(){ if(this.divReference) this.renderer.setStyle(this.divReference.nativeElement, 'color', 'red'); } } |
В Renderer2 много разных методов изменения элементов. Многие из них похожи на JavaScript DOM API. Угадать, что они делают, не должно вызвать проблем. Полный список методов можно найти в официальной документации.
4. Избегайте дублирующих провайдеров, перезаписывающих друг друга
Вы могли слышать, что Angular использует концепцию dependency injection. С помощью dependency injection можно запрашивать объекты сервисов в конструкторе.
Чтобы это заработало, сервисы или более широкие инъекции необходимо регистрировать в секции провайдера компонента или декоратора модуля. Самый распространенный метод – предоставить его на уровне модуля.
Проблема в том, что Angular использует иерархическую систему инъекции зависимостей. То есть сервисы/инъекции в корневом модуле (AppModule) доступны всем компонентам в этом модуле. Так как этот модуль должен содержать все другие компоненты и модули, сервисы доступны во всем приложении.
Если вы создаете сервис для подмодуля, он будет доступен только для этого подмодуля. Также если вы создаете сервисы в обоих модулях, компоненты в подмодуле получают объект сервиса, отличный от объекта любого другого компонента. Это может привести к любому виду ошибок, если вы думаете, что на ваш сервис создан один объект в приложении (singleton).
Решение простое. Создавайте сервисы один раз в AppModule. Если вы не знаете, что делать, придерживайтесь такого подхода. Особенно в начале. В 99% случаев он будет работать.
5. Angular Guards — не функция безопасности
Angular Guards – отличный способ искусственно ограничить доступ к определенным роутам. Например, для проверки авторизации пользователи еще до показа страницы. Пример такого guard:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Injectable } from '@angular/core'; import { AuthenticationService } from './authentication.service'; import { CanActivate } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthenticationService) {} canActivate() { return this.authService.isAuthenticated(); } } |
Так как guard не observable, его также нужно предоставить.
1 2 3 4 5 6 7 |
@NgModule({ providers: [ AuthGuard, AuthenticationService ] }) export class AppModule {} |
Осталось сказать ему, какие роуты защищать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@NgModule({ imports: [ RouterModule.forRoot([ { path: '', component: SomeComponent, canActivate: [AuthGuard] ]) ], providers: [ AuthGuard, AuthenticationService ] }) export class AppModule {} |
Проблема
Так что же за проблема с guards? Правда в том, что проблем с ними нет!
Но много людей путаются в названии. Они становятся проблемой, если люди неправильно понимают их назначение. Суть в том, что все, что вы делаете на стороне клиента, нельзя отнести к «безопасности». Вы отдаете потенциальным хакерам весь исходный код, и приложение может быть изменено как угодно. Т.е. наш guard можно обойти, закомментировав пару строк.
Конечно, не в AOT компиляции, но с достаточным упорством на это может понадобиться пару часов. Таким образом, данные, которые защищены лишь с помощью guard роута на стороне клиента, можно получить без лишних усилий. Вы, естественно, не хотите этого!
Решение
Если необходимо защитить любые важные данные, необходимо иметь настоящую защиту на сервере. Например, пописанный JavaScript Web Tokens.
6. Объявляйте компоненты только один раз
Чтобы компоненты работали в Angular, они должны быть объявлены в модуле. Так как у нас один модуль (AppModule), и мы регистрируем компоненты внутри него, это не проблема.
Но когда мы начинаем собирать наше приложение в модуль, что в любом случае нужно делать, вы, возможно, столкнетесь с общей проблемой.
Компонент можно объявлять только в одном модуле!
Это почти все, что нужно. Но что делать, если нам необходимо использовать компонент в нескольких модулях?
Решение простое. Просто оберните компонент в модуль. Возможно, одного модуля на компонент слишком много, так почему не создать модуль компонентов? Потом этот модуль можно будет импортировать в другие модули и использовать там компоненты.
Не забудьте объявить компоненты в модуле компонента, а также экспортировать их. В противном случае они будут доступны только из самого модуля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@NgModule({ imports: [ CommonModule ], declarations: [ LoginComponent, RegisterComponent, HelpComponent ], exports:[ LoginComponent, RegisterComponent, HelpComponent ] }) export class AuthenticationModule { } |
7. Ускорьте приложение с помощью *ngIf вместо атрибута [hidden]
Очередная распространенная ошибка – путаница с *ngIf и [hidden]. Правильный выбор может повысить производительность. Давайте рассмотрим обе техники.
Атрибут [hidden]
Атрибут hidden переключает видимость элемента. Как мы и ожидаем, так ведь? То есть если задать [hidden] в true, свойство CSS display задается в none. После этого элемент становится невидимым, но присутствует в DOM.
1 |
<div [hidden]="isHidden"></div> |
Проблема с атрибутом hidden в том, что выключенное CSS свойство можно легко переписать другим свойством, причем случайно. Например, если вы задали элементам display block, оно перепишет свойство display: none. То есть элемент будет всегда видимым.
Спасибо Kara Erickson, она указала на эту проблему. Более подробно по этой теме можно узнать в ее замечательной статье!
Другая теоретическая проблема – все элементы остаются в DOM, хотя они невидимы. Если речь идет о сотнях или тысячах элементов, они могут замедлить браузер. Так почему бы не удалять их, если они не нужны?
Директива *ngIf
Основное отличие директивы *ngIf в том, что вместо скрытия элементов она полностью удаляет их из DOM. Помимо возможного прироста производительности это решение также почище. Но это лишь мое мнение. Этот способ похож на стандартный способ скрытия элементов в Angular. Поэтому я почти всегда использую *ngIf.
1 |
<div *ngIf="!isHidden"></div> |
Недостатки директивы *ngIf – ее сложно дебажить, так как удаленный элементы уже нельзя инспектировать в DOM браузера.
8. Избегайте проблем с обслуживанием при оборачивании в сервисы
Вы могли заметить, что мы плавно перешли от критических ошибок к рекомендациям. Последний совет сделает выше приложение быстрее, меньше и удобнее в обслуживании.
Общий совет – всегда извлекайте базовую бизнес-логику в сервисы. Так код легче обслуживать, так как его можно убрать и заменить на новый подход за несколько секунд.
То же самое касается тестирования. Зачастую требуются сервисы, умеющие вытягивать внешние данные для подделки результатов при тестировании окружения. Если вы вытягиваете данные в сервисы, то все легко. Если нет, удачи с изменением кода.
Этого совета стоит обязательно придерживаться при использовании HttpClient. Он всегда должен быть завернут в централизованный сервис. Так он не только остается тестопригодным, но также так в него легко можно вносить изменения. Представьте, что ваш backend после недавнего обновления требует передавать с каждым запросом новый заголовок. Без централизованного сервиса вам придется искать все затронутые строки кода по всему приложению. Не нужно говорить, что это крайне не оптимально.
Вместо этого всегда оборачивайте http-запросы в сервисы. В худшем случае это никак вам не навредит. В лучшем – это сэкономит вам (и команде) часы на простейших задачах.
9. Повысьте производительность и уменьшите размер с помощью AOT в продакшн
Запустите Angular CLI приложение через
1 |
ng serve |
или
1 |
ng build |
Приложение соберется в обычном режиме. То есть приложение доставлено в браузер таким, какое оно есть. Далее браузер должен выполнить компилятор Angular, чтобы конвертировать компоненты и шаблоны в исполняемый JS код. Этот процесс не только занимает много времени, но и требует, чтобы с приложением доставлялся компилятор. В текущей версии Angular компилятор весит пример 1Мб (167Кб в сжатом виде). Это много!
Не верите? Можете сами проанализировать сборку Angular с помощью webpack-bundle-analyzer. Вам лишь нужно создать бандл с параметром stats-json.
1 |
ng build --stats-json |
Дальше запустите анализатор бандла:
1 |
webpack-bundle-analyzer dist/stats.json |
Инструмент автоматически откроет браузер и покажет вам похожий результат.
Решение
Используйте компиляцию AOT (Ahead of Time). В режиме AOT приложение компилируется в момент сборки. Браузеру уже этого делать не нужно. Мы делаем это один раз на все время работы с приложением. В результате приложение стартует намного быстрее.
Еще важнее, что размер бандла, который пользователи загружают, сильно снижается, так как теперь компилятор не включается в бандл.
В старых версиях angular-cli AOT нужно было включать вручную в продакшн билдах. С недавнего времени AOT активен по умолчанию в продакшн билдах. Вам лишь нужно добавить флаг продакшн в команду билда. Вы получаете не только AOT компиляцию, но и уменьшаете размер бандла. Старые версии
1 |
ng build --prod --aot |
Новые версии
1 |
ng build --prod |
Вот так выглядит продакшн бандл. В сжатом виде размер бандла составляет примерно 55Кб. От 330Кб до 55Кб. Вот это я называю улучшением! Также обратите внимание, что компилятор не включен в сборку.
10. Поддерживайте маленький вес приложения, импортируя только то, что нужно
Следующий совет связан с предыдущим. Мы опять будем смотреть на размер бандла. В этот раз мой совет – будьте аккуратны при импорте. Каждое выражение импорта увеличивает размер бандла. Теперь поняли? Чем больше кода, тем больше размер.
Проблема в том, что некоторые библиотеки огромны. При неправильном импорте можно подключить все библиотеку в приложение. Ниже показана популярная ошибка – импорт целой библиотеки RxJs в приложение.
1 |
import 'rxjs'; |
Эта маленькая трока удваивает размер приложения.
Суть в том, что дополнительный вес в этом случае совсем не обязателен. Сравните этот бандл с предыдущим, и вы заметите, что там так же подключен RxJs. Разница в то, что в предыдущих бандлах подключены только необходимые модули. С помощью этого импорта мы импортировали вообще все.
Решение
Выходов несколько. Основные решения – взвесить все добавленные в проект библиотеки. Вам реально нужна эта блестящая кнопка, которая добавляет 100Кб? Библиотека предлагает подмодули, с помощью которых можно импортировать только необходимый код? Если нет, возможно, оно того не стоит.
Если библиотека предлагает подмодули, проверьте, чтобы были подключены только необходимые вещи. Постоянно проверяйте размер бандла с помощью bundle-analyzer.
Как же импортировать только то, что нужно? Давайте разберем пример RxJs. RxJs разбил почти все в свои модули. Это заставит вас писать много импортов, но вес приложения будет маленьким. Например, вам нужно импортировать все используемые операторы:
1 2 3 |
import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; |
Никогда не делайте так:
1 |
import 'rxjs'; |
К сожалению, не все библиотеки разбивают свой код. И все они делают это по-разному. Более тщательное изучение своих библиотек – ключ к маленькому и быстрому приложению.
11. Не создавайте утечек памяти – отписывайтесь от подписок
При работе с RxJs Observables и Subscriptions легко можно получить утечку памяти. Это происходит потому, что вы уничтожили компонент, а функцию, зарегистрированную в observable, нет. Так вы не только получаете утечку памяти, но также можете столкнуться со странным поведением.
Решение
Чтобы избежать подобных ситуаций, отписывайтесь от подписок при уничтожении компонента. Это удобно делать внутри ngOnDestroy. Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Component({}) export class ExampleComponent implements OnDestroy{ private subscriptions = []; constructor(){ this.subscriptions.push(this.anyObservable.subscribe()); } ngOnDestroy(){ for(let subscription of this.subscriptions){ subscriptions.unsubscribe(); } } } |
Заключение
В этой статье мы разобрали все ошибки, с которыми часто сталкиваются новички. По крайней мере, я делал их… Надеюсь, мои ошибки помогли вам полностью избежать их и обеспечили вам лучший опыт разработки на Angular.
Автор: Lukas Marx
Источник: //malcoded.com/
Редакция: Команда webformyself.