Все возможности, никаких хлопот: учебник по 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 и чистоты кода.
В результате у нас есть базовая рабочая папка и файлы.
Теперь давайте добавим новое приложение. Для этого мы запустим:
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
, мы увидим, что приложение работает с исходным каркасом.
Если мы запустим ng build --prod
, мы увидим список сгенерированных файлов.
У нас есть две версии каждого файла. Один совместим с устаревшими браузерами, а другой скомпилирован для ES2015, который использует более новые API и требует меньше полифилов для работы в браузерах.
Одним из больших улучшений Angular 9 является размер пакета. По словам команды Angular, вы можете увидеть снижение до 40% для больших приложений.
Для недавно созданного приложения размер пакета очень похож на размер пакета Angular 8, но по мере роста вашего приложения вы увидите, что размер пакета становится меньше по сравнению с предыдущими версиями.
Еще одна функция, представленная в Angular 9, — это возможность предупреждать нас, если какой-либо из файлов CSS стиля компонента превышает определенный порог.
Это поможет нам отловить некачественный импорт стилей или огромные файлы стилей компонентов.
Добавление формы для оценки телешоу
Далее мы добавим форму для оценки телешоу. Для этого сначала установим 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 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' } ];
… мы получим ошибку от компилятора.
Эта проверка типов позволит нам предотвратить опечатки и неправильное использование типов 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; }
Если мы внесем эти три изменения, мы получим другой тип ошибки от компилятора.
Если мы хотим обойти это, мы можем использовать $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
укажет нам, где ошибка.
Мы можем исправить эту ошибку, обернув вызов службы с помощью 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
, наш тест проходит.
Вернемся к нашему примеру приложения 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(); } }
Теперь, когда мы перейдем в консоль Firebase, мы увидим только что созданный элемент.
Наконец, давайте перечислим все рейтинги в 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 и Angular Ivy: лучшая разработка, лучшие приложения и лучшая совместимость
В этом руководстве по Angular 9 мы рассмотрели создание базовой формы, сохранение данных в Firebase и извлечение из нее элементов.
Попутно мы увидели, какие улучшения и новые функции включены в Angular 9 и Angular Ivy. Полный список можно найти в последней публикации официального блога Angular.
Как партнер Google Cloud, специалисты Toptal, сертифицированные Google, доступны для компаний по запросу для их наиболее важных проектов.