Все возможности, никаких хлопот: учебник по Angular 9

Опубликовано: 2022-03-11

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

Но это не так! Команда Angular полностью переработала свой компилятор, что привело к более быстрой сборке, более быстрому запуску тестов, меньшим размерам пакетов и, что наиболее важно, к обратной совместимости со старыми версиями. С Angular 9 разработчики в основном получают все преимущества без каких-либо хлопот.

В этом руководстве по Angular 9 мы создадим приложение Angular с нуля. Мы будем использовать некоторые из последних функций Angular 9 и попутно рассмотрим другие улучшения.

Учебник по Angular 9: запуск нового приложения Angular

Давайте начнем с нашего примера проекта Angular. Во-первых, давайте установим последнюю версию CLI Angular:

 npm install -g @angular/cli

Мы можем проверить версию Angular CLI, запустив ng version .

Далее создадим приложение Angular:

 ng new ng9-app --create-application=false --strict

Мы используем два аргумента в нашей ng new :

  • --create-application=false укажет CLI создавать только файлы рабочей области. Это поможет нам лучше организовать наш код, когда нам нужно иметь более одного приложения и несколько библиотек.
  • --strict добавит более строгие правила для обеспечения большей типизации TypeScript и чистоты кода.

В результате у нас есть базовая рабочая папка и файлы.

Снимок экрана IDE, показывающий папку ng9-app, содержащую node_modules, .editorconfig, .gitignore, angular.json, package-lock.json, package.json, README.md, tsconfig.json и tslint.json.

Теперь давайте добавим новое приложение. Для этого мы запустим:

 ng generate application tv-show-rating

Нам будет предложено:

 ? Would you like to share anonymous usage data about this project with the Angular Team at Google under Google's Privacy Policy at https://policies.google.com/privacy? For more details and how to change this setting, see http://angular.io/analytics. No ? Would you like to add Angular routing? Yes ? Which stylesheet format would you like to use? SCSS

Теперь, если мы запустим ng serve , мы увидим, что приложение работает с исходным каркасом.

Скриншот строительных лесов Angular 9 с уведомлением о том, что «приложение для рейтинга телешоу запущено!» Есть также ссылки на ресурсы и дальнейшие шаги.

Если мы запустим ng build --prod , мы увидим список сгенерированных файлов.

Скриншот вывода Angular 9 «ng build --prod». Он начинается с «Создание пакетов ES5 для дифференциальной загрузки…». После этого в нем перечислены несколько фрагментов файлов JavaScript — среда выполнения, полифиллы и основной, каждый с версией -es2015 и -es5 — и один файл CSS. Последняя строка дает временную метку, хэш и время выполнения 23 881 миллисекунду.

У нас есть две версии каждого файла. Один совместим с устаревшими браузерами, а другой скомпилирован для ES2015, который использует более новые API и требует меньше полифилов для работы в браузерах.

Одним из больших улучшений Angular 9 является размер пакета. По словам команды Angular, вы можете увидеть снижение до 40% для больших приложений.

Для недавно созданного приложения размер пакета очень похож на размер пакета Angular 8, но по мере роста вашего приложения вы увидите, что размер пакета становится меньше по сравнению с предыдущими версиями.

Еще одна функция, представленная в Angular 9, — это возможность предупреждать нас, если какой-либо из файлов CSS стиля компонента превышает определенный порог.

Снимок экрана раздела «бюджеты» файла конфигурации Angular 9 JSON с двумя объектами в массиве. Первый объект имеет «тип», установленный на «начальный», «maximumWarning» — на «2 МБ» и «maximumError» — на «5 МБ». Второй объект имеет «тип» со значением «anyComponentStyle», «maximumWarning» со значением «6 КБ» и «maximumError» со значением «10 КБ».

Это поможет нам отловить некачественный импорт стилей или огромные файлы стилей компонентов.

Добавление формы для оценки телешоу

Далее мы добавим форму для оценки телешоу. Для этого сначала установим bootstrap и ng-bootstrap :

 npm install bootstrap @ng-bootstrap/ng-bootstrap

Еще одно улучшение Angular 9 — i18n (интернационализация). Раньше разработчикам нужно было запускать полную сборку для каждой локали в приложении. Вместо этого Angular 9 позволяет нам создать приложение один раз и сгенерировать все файлы i18n в процессе после сборки, что значительно сокращает время сборки. Поскольку ng-bootstrap зависит от i18n, мы добавим новый пакет в наш проект:

 ng add @angular/localize

Далее мы добавим тему Bootstrap в styles.scss нашего приложения:

 @import "~bootstrap/scss/bootstrap";

И мы включим NgbModule и ReactiveFormsModule в наш AppModule на app.module.ts :

 // ... import { ReactiveFormsModule } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @NgModule({ imports: [ // ... ReactiveFormsModule, NgbModule ], })

Далее мы добавим в app.component.html базовую сетку для нашей формы:

 <div class="container"> <div class="row"> <div class="col-6"> </div> </div> </div>

И создайте компонент формы:

 ng gc TvRatingForm

tv-rating-form.component.html и добавим форму для оценки телепередач.

 <form [formGroup]="form" (ngSubmit)="submit()" class="mt-3"> <div class="form-group"> <label>TV SHOW</label> <select class="custom-select" formControlName="tvShow"> <option *ngFor="let tvShow of tvShows" [value]="tvShow.name">{{tvShow.name}}</option> </select> </div> <div class="form-group"> <ngb-rating [max]="5" formControlName="rating"></ngb-rating> </div> <button [disabled]="form.invalid || form.disabled" class="btn btn-primary">OK</button> </form>

А tv-rating-form.component.ts будет выглядеть так:

 // ... export class TvRatingFormComponent implements OnInit { tvShows = [ { name: 'Better call Saul!' }, { name: 'Breaking Bad' }, { name: 'Lost' }, { name: 'Mad men' } ]; form = new FormGroup({ tvShow: new FormControl('', Validators.required), rating: new FormControl('', Validators.required), }); submit() { alert(JSON.stringify(this.form.value)); this.form.reset(); } }

Наконец, давайте добавим форму в app.component.html :

 <!-- ... --> <div class="col-6"> <app-tv-rating-form></app-tv-rating-form> </div>

На данный момент у нас есть некоторые базовые функции пользовательского интерфейса. Теперь, если мы снова запустим ng serve , мы увидим его в действии.

Скриншот учебного приложения Angular 9, показывающего форму под названием «ТВ-ШОУ» с раскрывающимся списком нескольких названий шоу, звездным счетчиком и кнопкой «ОК». В анимации пользователь выбирает шоу, выбирает рейтинг, а затем нажимает кнопку «ОК».

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

Отладка с помощью Angular 9 Ivy

Еще одно большое улучшение, представленное в Angular 9 и Angular Ivy, — это возможности отладки. Теперь компилятор может обнаруживать больше ошибок и выдавать их в более «читаемом» виде.

Давайте посмотрим на это в действии. Во-первых, мы активируем проверку шаблонов в tsconfig.json :

 { // ... "angularCompilerOptions": { "fullTemplateTypeCheck": true, "strictInjectionParameters": true, "strictTemplates": true } }

Теперь, если мы обновим массив tvShows и переименуем name в title :

 tvShows = [ { title: 'Better call Saul!' }, { title: 'Breaking Bad' }, { title: 'Lost' }, { title: 'Mad men' } ];

… мы получим ошибку от компилятора.

Скриншот выходных данных компилятора Angular 9/Angular Ivy с именем файла и позицией, в котором говорится: «Ошибка TS2339: свойство 'имя' не существует для типа '{ title: string; }'». Он также показывает рассматриваемую строку кода и подчеркивает ссылку, в данном случае в файле tv-rating-form.component.html, где упоминается tvShow.name. После этого ссылка на этот файл HTML прослеживается до соответствующего файла TypeScript и аналогичным образом выделяется.

Эта проверка типов позволит нам предотвратить опечатки и неправильное использование типов TypeScript.

Проверка Angular Ivy для @Input()

Еще одна хорошая проверка, которую мы получаем, — это @Input() . Например, мы можем добавить это в tv-rating-form.component.ts :

 @Input() title: string;

… и привяжите его в app.component.html :

 <app-tv-rating-form [title]="title"></app-tv-rating-form>

…и затем измените app.component.ts следующим образом:

 // ... export class AppComponent { title = null; }

Если мы внесем эти три изменения, мы получим другой тип ошибки от компилятора.

Скриншот выходных данных компилятора Angular 9/Angular Ivy в формате, аналогичном предыдущему, с выделением app.component.html с «ошибкой TS 2322: тип 'null' не может быть назначен типу 'string'».

Если мы хотим обойти это, мы можем использовать $any() в шаблоне, чтобы привести значение к any и исправить ошибку:

 <app-tv-rating-form [title]="$any(title)"></app-tv-rating-form>

Однако правильным способом исправить это было бы сделать title в форме обнуляемым:

 @Input() title: string | null ;

ExpressionChangedAfterItHasBeenCheckedError в Angular 9 Ivy

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

Итак, давайте представим ошибку ExpressionChangedAfterItHasBeenCheckedError . Для этого сначала создадим сервис:

 ng gs Title

Далее мы добавим BehaviorSubject и методы для доступа к Observable и генерации нового значения.

 export class TitleService { private bs = new BehaviorSubject < string > (''); constructor() {} get title$() { return this.bs.asObservable(); } update(title: string) { this.bs.next(title); } }

После этого мы добавим это в app.component.html :

 <!-- ... --> <div class="col-6"> <h2> {{title$ | async}} </h2> <app-tv-rating-form [title]="title"></app-tv-rating-form> </div>

И в app.component.ts мы добавим TitleService :

 export class AppComponent implements OnInit { // ... title$: Observable < string > ; constructor( private titleSvc: TitleService ) {} ngOnInit() { this.title$ = this.titleSvc.title$; } // ... }

Наконец, в tv-rating-form.component.ts мы TitleService и обновим заголовок AppComponent , что вызовет ошибку ExpressionChangedAfterItHasBeenCheckedError .

 // ... constructor( private titleSvc: TitleService ) { } ngOnInit() { this.titleSvc.update('new title!'); }

Теперь мы можем увидеть подробную информацию об ошибке в консоли разработчика браузера, и нажатие на app.component.html укажет нам, где ошибка.

Скриншот консоли разработчика браузера, показывающий отчет Angular Ivy об ошибке ExpressionChangedAfterItHasBeenCheckedError. Трассировка стека красным текстом показывает ошибку, а также предыдущие и текущие значения и подсказку. В середине трассировки стека находится единственная строка, не относящаяся к core.js. Пользователь щелкает по нему и попадает на строку app.component.html, которая вызывает ошибку.

Мы можем исправить эту ошибку, обернув вызов службы с помощью setTimeout :

 setTimeout(() => { this.titleSvc.update('new title!'); });

Чтобы понять, почему возникает ошибка ExpressionChangedAfterItHasBeenCheckedError , и изучить другие возможности, стоит прочитать пост Максима Корецкого на эту тему.

Angular Ivy позволяет нам более четко представлять ошибки и помогает обеспечить типизацию TypeScript в нашем коде. В следующем разделе мы рассмотрим некоторые распространенные сценарии, в которых мы воспользуемся преимуществами Ivy и отладки.

Написание теста для нашего приложения Angular 9 с использованием компонентов

В Angular 9 был представлен новый API для тестирования, называемый компонентными жгутами . Идея заключается в том, чтобы удалить всю рутинную работу, необходимую для взаимодействия с DOM, что значительно упрощает работу и делает работу более стабильной.

API-интерфейс компонентов включен в библиотеку @angular/cdk , поэтому давайте сначала установим его в нашем проекте:

 npm install @angular/cdk

Теперь мы можем написать тест и использовать жгуты компонентов. В tv-rating-form.component.spec.ts тест:

 import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { ReactiveFormsModule } from '@angular/forms'; describe('TvRatingFormComponent', () => { let component: TvRatingFormComponent; let fixture: ComponentFixture < TvRatingFormComponent > ; beforeEach(async (() => { TestBed.configureTestingModule({ imports: [ NgbModule, ReactiveFormsModule ], declarations: [TvRatingFormComponent] }).compileComponents(); })); // ... });

Далее давайте реализуем ComponentHarness для нашего компонента. Мы собираемся создать две обвязки: одну для TvRatingForm и другую для NgbRating . Для ComponentHarness требуется static поле hostSelector , которое должно принимать значение селектора компонента.

 // ... import { ComponentHarness, HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; class TvRatingFormHarness extends ComponentHarness { static hostSelector = 'app-tv-rating-form'; } class NgbRatingHarness extends ComponentHarness { static hostSelector = 'ngb-rating'; } // ...

Для нашего TvRatingFormHarness мы создадим селектор для кнопки отправки и функцию для запуска click . Вы можете видеть, насколько проще становится реализовать это.

 class TvRatingFormHarness extends ComponentHarness { // ... protected getButton = this.locatorFor('button'); async submit() { const button = await this.getButton(); await button.click(); } }

Далее мы добавим методы для установки рейтинга. Здесь мы используем locatorForAll для поиска всех элементов <span> , которые представляют звезды, на которые пользователь может щелкнуть. Функция rate просто получает все возможные звезды рейтинга и нажимает на ту, которая соответствует отправленному значению.

 class NgbRatingHarness extends ComponentHarness { // ... protected getRatings = this.locatorForAll('span:not(.sr-only)'); async rate(value: number) { const ratings = await this.getRatings(); return ratings[value - 1].click(); } }

Последнее, чего не хватает, — это подключить TvRatingFormHarness к NgbRatingHarness . Для этого мы просто добавляем локатор в класс TvRatingFormHarness .

 class TvRatingFormHarness extends ComponentHarness { // ... getRating = this.locatorFor(NgbRatingHarness); // ... }

Теперь давайте напишем наш тест:

 describe('TvRatingFormComponent', () => { // ... it('should pop an alert on submit', async () => { spyOn(window, 'alert'); const select = fixture.debugElement.query(By.css('select')).nativeElement; select.value = 'Lost'; select.dispatchEvent(new Event('change')); fixture.detectChanges(); const harness = await TestbedHarnessEnvironment.harnessForFixture(fixture, TvRatingFormHarness); const rating = await harness.getRating(); await rating.rate(1); await harness.submit(); expect(window.alert).toHaveBeenCalledWith('{"tvShow":"Lost","rating":1}'); }); });

Обратите внимание, что для нашего select в форме мы не реализовали установку его значения через обвязку. Это потому, что API по-прежнему не поддерживает выбор параметра. Но это дает нам возможность сравнить, как выглядело взаимодействие с элементами до использования компонентов.

И последнее, прежде чем мы запустим тесты. Нам нужно исправить app.component.spec.ts , так как мы обновили title , чтобы он был null .

 describe('AppComponent', () => { // ... it(`should have as title 'tv-show-rating'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app.title).toEqual(null); }); });

Теперь, когда мы запускаем ng test , наш тест проходит.

Скриншот, на котором Karma запускает тесты в нашем приложении Angular 9. Он показывает «Выполнено 2 из 6 спецификаций» с сообщением «Неполное: найдено соответствие () или fdescribe (), 2 спецификации, 0 сбоев, рандомизировано с начальным числом 69573». Два теста TvRatingFormComponent выделены. Все три теста AppComponent и один тест TitleService выделены серым цветом.

Вернемся к нашему примеру приложения Angular 9: сохранение данных в базе данных

Давайте завершим наш учебник по Angular 9, добавив подключение к Firestore и сохранив рейтинги в базе данных.

Для этого нам нужно создать проект Firebase. Затем мы установим необходимые зависимости.

 npm install @angular/fire firebase

В настройках проекта Firebase Console мы получим его конфигурацию и добавим их в environment.ts и environment.prod.ts :

 export const environment = { // ... firebase: { apiKey: '{your-api-key}', authDomain: '{your-project-id}.firebaseapp.com', databaseURL: 'https://{your-project-id}.firebaseio.com', projectId: '{your-project-id}', storageBucket: '{your-project-id}.appspot.com', messagingSenderId: '{your-messaging-id}', appId: '{your-app-id}' } };

После этого импортируем необходимые модули в app.module.ts :

 import { AngularFireModule } from '@angular/fire'; import { AngularFirestoreModule } from '@angular/fire/firestore'; import { environment } from '../environments/environment'; @NgModule({ // ... imports: [ // ... AngularFireModule.initializeApp(environment.firebase), AngularFirestoreModule, ], // ... })

Далее, в tv-rating-form.component.ts мы внедрим сервис AngularFirestore и сохраним новый рейтинг при отправке формы:

 import { AngularFirestore } from '@angular/fire/firestore'; export class TvRatingFormComponent implements OnInit { constructor( // ... private af: AngularFirestore, ) { } async submit(event: any) { this.form.disable(); await this.af.collection('ratings').add(this.form.value); this.form.enable(); this.form.reset(); } } 

Скриншот учебного приложения Angular 9, показывающий форму под названием «ТВ-ШОУ» под более крупным заголовком страницы «Новое название!» Опять же, у него есть раскрывающийся список с несколькими названиями шоу, счетчик звезд и кнопка «ОК», и снова пользователь выбирает шоу, выбирает рейтинг и затем нажимает кнопку «ОК».

Теперь, когда мы перейдем в консоль Firebase, мы увидим только что созданный элемент.

Скриншот консоли Firebase. В левой колонке joaq-lab с некоторыми коллекциями: участники, гонки, рейтинги, тестирование и пользователи. Элемент оценок выбран и отображается в среднем столбце с выбранным идентификатором — это единственный документ. В правом столбце показаны два поля: «рейтинг» установлен на 4, а «tvShow» установлен на «Безумцы».

Наконец, давайте перечислим все рейтинги в AppComponent . Для этого в app.component.ts мы получим данные из коллекции:

 import { AngularFirestore } from '@angular/fire/firestore'; export class AppComponent implements OnInit { // ... ratings$: Observable<any>; constructor( // ... private af: AngularFirestore ) { } ngOnInit() { // ... this.ratings$ = this.af.collection('ratings').valueChanges(); } }

…и в app.component.html мы добавим список рейтингов:

 <div class="container"> <div class="row"> // ... <div class="col-6"> <div> <p *ngFor="let rating of ratings$ | async"> {{rating.tvShow}} ({{rating.rating}}) </p> </div> </div> </div> </div>

Вот как выглядит наше учебное приложение по Angular 9, когда все собрано вместе.

Скриншот учебного приложения Angular 9, показывающий форму под названием «ТВ-ШОУ» под более крупным заголовком страницы «Новое название!» Опять же, у него есть раскрывающийся список, в котором перечислены несколько названий шоу, счетчик звезд и кнопка «ОК». На этот раз в правой колонке уже перечислены «Безумцы (4)», и пользователь оценивает Lost в три звезды, а затем «Безумцы» снова в четыре звезды. Правая колонка остается в алфавитном порядке после обеих новых оценок.

Angular 9 и Angular Ivy: лучшая разработка, лучшие приложения и лучшая совместимость

В этом руководстве по Angular 9 мы рассмотрели создание базовой формы, сохранение данных в Firebase и извлечение из нее элементов.

Попутно мы увидели, какие улучшения и новые функции включены в Angular 9 и Angular Ivy. Полный список можно найти в последней публикации официального блога Angular.


Значок партнера Google Cloud.

Как партнер Google Cloud, специалисты Toptal, сертифицированные Google, доступны для компаний по запросу для их наиболее важных проектов.