所有特权,无后顾之忧: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 类型和代码清洁度。
因此,我们有一个基本的工作区文件夹和文件。
现在,让我们添加一个新应用。 为此,我们将运行:
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 并且需要更少的 polyfill 在浏览器上运行。
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";
我们将在AppModule
上的app.module.ts
中包含NgbModule
和ReactiveFormsModule
:
// ... 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 新特性,这些特性是为了帮助调试而添加的。 由于这是我们日常工作中非常常见的任务,因此值得了解发生了什么变化以使我们的生活更轻松一些。
使用 Angular 9 Ivy 进行调试
Angular 9 和 Angular Ivy 中引入的另一个重大改进是调试体验。 编译器现在可以检测到更多错误并以更“可读”的方式抛出它们。
让我们看看它的实际效果。 首先,我们将在tsconfig.json
中激活模板检查:
{ // ... "angularCompilerOptions": { "fullTemplateTypeCheck": true, "strictInjectionParameters": true, "strictTemplates": true } }
现在,如果我们更新tvShows
数组并将 name 重name
为title
:
tvShows = [ { title: 'Better call Saul!' }, { title: 'Breaking Bad' }, { title: 'Lost' }, { title: 'Mad men' } ];
…我们会从编译器中得到一个错误。
这种类型检查将允许我们防止打字错误和 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; }
如果我们进行这三个更改,我们将从编译器中得到另一种类型的错误。
如果我们想绕过它,我们可以在模板上使用$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
会指出错误所在。
我们可以通过使用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
,另一个用于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 示例应用程序:将数据保存在数据库中
让我们通过添加到 Firestore 的连接并将评级保存在数据库中来结束我们的 Angular 9 教程。
为此,我们需要创建一个 Firebase 项目。 然后,我们将安装所需的依赖项。
npm install @angular/fire firebase
在 Firebase 控制台的项目设置中,我们将获取其配置并将它们添加到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
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 认证专家可根据公司最重要项目的需求提供给他们。