所有特权,无后顾之忧:Angular 9 教程

已发表: 2022-03-11

“每年互联网都会中断,”俗话说,开发人员通常不得不去修复它。 对于期待已久的 Angular 版本 9,人们可能会认为这将适用,在早期版本上开发的应用程序将需要经历一个主要的迁移过程。

但事实并非如此! Angular 团队完全重新设计了其编译器,从而实现了更快的构建、更快的测试运行、更小的包大小,最重要的是,与旧版本的向后兼容性。 使用 Angular 9,开发人员基本上可以毫不费力地获得所有好处。

在这个 Angular 9 教程中,我们将从头开始构建一个 Angular 应用程序。 我们将使用一些最新的 Angular 9 功能,并在此过程中介绍其他改进。

Angular 9 教程:从一个新的 Angular 应用程序开始

让我们开始我们的 Angular 项目示例。 首先,让我们安装最新版本的 Angular 的 CLI:

 npm install -g @angular/cli

我们可以通过运行ng version来验证 Angular CLI 版本。

接下来,让我们创建一个 Angular 应用程序:

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

我们在ng new命令中使用了两个参数:

  • --create-application=false将告诉 CLI 仅生成工作区文件。 当我们需要拥有多个应用程序和多个库时,这将帮助我们更好地组织代码。
  • --strict将添加更严格的规则以强制执行更多的 TypeScript 类型和代码清洁度。

因此,我们有一个基本的工作区文件夹和文件。

显示 ng9-app 文件夹的 IDE 屏幕截图,其中包含 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 的脚手架屏幕截图,带有“tv-show-rating app is running!”的通知。还有指向资源和后续步骤的链接。

如果我们运行ng build --prod ,我们可以看到生成的文件列表。

Angular 9 的“ng build --prod”输出的截图。它以“为差异加载生成 ES5 包......”开始,完成之后,它列出了几个 JavaScript 文件块——运行时、polyfill 和 main,每个都有 -es2015 和 -es5 版本——以及一个 CSS 文件。最后一行给出了时间戳、哈希和 23,881 毫秒的运行时间。

我们有每个文件的两个版本。 一种与旧版浏览器兼容,另一种是针对 ES2015 编译的,它使用更新的 API 并且需要更少的 polyfill 在浏览器上运行。

Angular 9 的一大改进是包大小。 根据 Angular 团队的说法,您可以看到大型应用程序的降幅高达 40%。

对于新创建的应用程序,包大小与 Angular 8 非常相似,但随着应用程序的增长,您会看到包大小与以前的版本相比变得更小。

Angular 9 中引入的另一个功能是,如果任何组件样式的 CSS 文件大于定义的阈值,则能够警告我们。

Angular 9 JSON 配置文件的“预算”部分的屏幕截图,数组中有两个对象。第一个对象的“type”设置为“initial”,“maximumWarning”设置为“2mb”,“maximumError”设置为“5mb”。第二个对象的“type”设置为“anyComponentStyle”,“maximumWarning”设置为“6kb”,“maximumError”设置为“10kb”。

这将帮助我们捕获错误的样式导入或巨大的组件样式文件。

添加表格以评价电视节目

接下来,我们将添加一个表单来为电视节目评分。 为此,首先,我们将安装bootstrapng-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";

我们将在AppModule上的app.module.ts中包含NgbModuleReactiveFormsModule

 // ... 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>

至此,我们有了一些基本的 UI 功能。 现在,如果我们再次运行ng serve ,我们可以看到它的实际效果。

Angular 9 教程应用程序的屏幕截图,显示了一个名为“TV SHOW”的表单,下拉列表中列出了一些节目标题、一个星表和一个 OK 按钮。在动画中,用户选择一个节目,选择一个评级,然后单击“确定”按钮。

在继续之前,让我们快速浏览一些有趣的 Angular 9 新特性,这些特性是为了帮助调试而添加的。 由于这是我们日常工作中非常常见的任务,因此值得了解发生了什么变化以使我们的生活更轻松一些。

使用 Angular 9 Ivy 进行调试

Angular 9 和 Angular Ivy 中引入的另一个重大改进是调试体验。 编译器现在可以检测到更多错误并以更“可读”的方式抛出它们。

让我们看看它的实际效果。 首先,我们将在tsconfig.json中激活模板检查:

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

现在,如果我们更新tvShows数组并将 name 重nametitle

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

…我们会从编译器中得到一个错误。

Angular 9/Angular Ivy 编译器输出的屏幕截图,带有文件名和位置,显示“错误 TS2339:类型 '{ title: string; }' 上不存在属性 'name'”。它还显示了有问题的代码行并在引用下划线,在这种情况下,在 tv-rating-form.component.html 文件中提到了 tvShow.name。之后,对这个 HTML 文件的引用被追踪到相应的 TypeScript 文件并同样突出显示。

这种类型检查将允许我们防止打字错误和 TypeScript 类型的错误使用。

@Input()的 Angular Ivy 验证

我们得到的另一个很好的验证是@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 ;

Angular 9 Ivy 中的ExpressionChangedAfterItHasBeenCheckedError

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 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错误发生的原因并探索其他可能性,Maxim Koretskyi 关于该主题的帖子值得一读。

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 ,另一个用于NgbRatingComponentHarness需要一个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 应用程序上运行测试的屏幕截图。它显示“Ran 2 of 6 specs”和消息“Incomplete: fit() or fdescribe() was found, 2 specs, 0 failures, randomized with seed 69573.” TvRatingFormComponent 的两个测试被突出显示。 AppComponent 的三项测试和 TitleService 的一项测试都是灰色的。

回到我们的 Angular 9 示例应用程序:将数据保存在数据库中

让我们通过添加到 Firestore 的连接并将评级保存在数据库中来结束我们的 Angular 9 教程。

为此,我们需要创建一个 Firebase 项目。 然后,我们将安装所需的依赖项。

 npm install @angular/fire firebase

在 Firebase 控制台的项目设置中,我们将获取其配置并将它们添加到environment.tsenvironment.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 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 教程应用程序的屏幕截图,在较大的页面标题“新标题!”下方显示标题为“电视节目”的表单同样,它有一个下拉列表,列出了一些节目标题、一个星表和一个 OK 按钮,用户再次选择一个节目,选择一个评级,然后单击 OK 按钮。

现在,当我们转到 Firebase 控制台时,我们将看到新创建的项目。

Firebase 控制台的屏幕截图。左栏中是 joaq-lab,其中包含一些集合:参加者、比赛、评级、测试和用户。评级项目被选中,并显示在中间列中,并带有一个选定的 ID——它是唯一的文档。右栏显示两个字段:“rating”设置为 4,“tvShow”设置为“Mad men”。

最后,让我们在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 教程应用程序的屏幕截图,在较大的页面标题“新标题!”下方显示标题为“电视节目”的表单同样,它有一个下拉列表,列出了一些节目标题、一个星表和一个确定按钮。这一次,右侧的一栏已经列出了“Mad men (4)”,用户对 Lost 的评价为三颗星,其次是“Mad men”,再次为四颗星。在两个新评级之后,右侧列仍按字母顺序排列。

Angular 9 和 Angular Ivy:更好的开发、更好的应用和更好的兼容性

在这个 Angular 9 教程中,我们介绍了构建基本表单、将数据保存到 Firebase 以及从中检索项目。

在此过程中,我们看到了 Angular 9 和 Angular Ivy 中包含了哪些改进和新功能。 如需完整列表,您可以查看官方 Angular 博客的最新发布帖子。


Google Cloud 合作伙伴徽章。

作为 Google Cloud 合作伙伴,Toptal 的 Google 认证专家可根据公司最重要项目的需求提供给他们。