angular8官网阅读小记

最近angular刚刚升上8.0版本,寻思着仔细阅读一下官网,查漏补缺。

官网教程——英雄指南实践

官网例子实践

使用In-memory Web API模拟数据服务器

  1. 从 npm 中安装这个内存 Web API 包

    1
    npm install angular-in-memory-web-api --save
  2. 导入 HttpClientInMemoryWebApiModule 和 InMemoryDataService 类(你很快就要创建它)。

    1
    2
    3
    4
    5
    6
    7
    8
    HttpClientModule,

    // The HttpClientInMemoryWebApiModule module intercepts HTTP requests
    // and returns simulated server responses.
    // Remove it when a real server is ready to receive requests.
    HttpClientInMemoryWebApiModule.forRoot(
    InMemoryDataService, { dataEncapsulation: false }
    )
  3. src/app/in-memory-data.service.ts 类是通过下列命令生成的:

    1
    ng generate service InMemoryData

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}

// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}

AsyncPipe

1
<li *ngFor="let hero of heroes$ | async" >
  • $ 是一个命名惯例,用来表明 heroes$ 是一个 Observable,而不是数组。
  • | async表示 Angular 的 AsyncPipe, 会自动订阅到 Observable,这样你就不用再在组件类中订阅了。

核心知识

架构

NgModule 简介

NgModule 是一个带有 @NgModule() 装饰器的类。@NgModule() 装饰器是一个函数,它接受一个元数据对象,该对象的属性用来描述这个模块。其中最重要的属性如下。

  • declarations(可声明对象表) —— 那些属于本 NgModule 的组件、指令、管道。
  • exports(导出表) —— 那些能在其它模块的组件模板中使用的可声明对象的子集。
  • imports(导入表) —— 那些导出了本模块中的组件模板所需的类的其它模块。
  • providers —— 本模块向全局服务中贡献的那些服务的创建器。 这些服务能被本应用中的任何部分使用。(你也可以在组件级别指定服务提供商,这通常是首选方式。)
  • bootstrap —— 应用的主视图,称为根组件。它是应用中所有其它视图的宿主。只有根模块才应该设置这个 bootstrap 属性。

NgModule 和 JavaScript 的模块

NgModule 系统与 JavaScript(ES2015)用来管理 JavaScript 对象的模块系统,两者不同但互补。你可以使用它们来共同编写你的应用。

在JavaScript中,每个文件是一个模块,文件中定义的所有对象都从属于那个模块。通过export关键字,模块可以把它的某些对象声明为公共的。其它JavaScript模块可以使用import语句来访问这些公共对象。

服务与依赖注入简介

服务是一个广义的概念,它包括应用所需的任何值、函数或特性。狭义的服务是一个明确定义了用途的类。

组件与模板

模板语法

HTML是Angular 模板的语言。几乎所有的 HTML 语法都是有效的模板语法。 但值得注意的例外是 <script> 元素,它被禁用了,以阻止脚本注入攻击的风险。

NgModel - 使用[(ngModel)]双向绑定到表单元素

ngModel展开形式用法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.标准
<input [(ngModel)]="currentHero.name">

// 2.展开
<input
[ngModel]="currentHero.name"
(ngModelChange)="currentHero.name=$event">

// 3.
// 下面这个生造的例子强制输入框的内容变成大写:
<input
[ngModel]="currentHero.name"
(ngModelChange)="setUppercaseName($event)">

带 trackBy 的 ngFor

避免ngFor刷新整个列表

ngFor指令有时候会性能较差,特别是在大型列表中。对一个条目的一丁点改动、移除或添加,都会导致级联的 DOM 操作。在 Angular 看来,它只是一个由新的对象引用构成的新列表。
如果给它指定一个 trackBy,Angular就可以避免这种折腾。往组件中添加一个方法,它会返回 NgFor应该追踪的值。

1
2
// ts
trackByHeroes(index: number, hero: Hero): number { return hero.id; }
1
2
3
4
// html
<div *ngFor="let hero of heroes; trackBy: trackByHeroes">
({{hero.id}}) {{hero.name}}
</div>
  • 如果没有 trackBy,这些按钮都会触发完全的 DOM 元素替换。有了 trackBy,则只有修改了 id 的按钮才会触发元素替换。

非空断言操作符(!)

如果类型检查器在运行期间无法确定一个变量是 null 或 undefined,那么它也会抛出一个错误。 你自己可能知道它不会为空,但类型检查器不知道。 所以你要告诉类型检查器,它不会为空,这时就要用到非空断言操作符。

1
2
3
4
5
// 与安全导航操作符不同的是,非空断言操作符不会防止出现 null 或 undefined。 它只是告诉 TypeScript 的类型检查器对特定的属性表达式,不做 "严格空值检测"。
<!--No hero, no text -->
<div *ngIf="hero">
The hero's name is {{hero!.name}}
</div>

类型转换函数 $any ($any( <表达式> ))

有时候,绑定表达式可能会报类型错误,并且它不能或很难指定类型。要消除这种报错,你可以使用 $any 转换函数来把表达式转换成 any 类型。

1
2
3
4
5
6
7
8
9
10
11
<!-- Accessing an undeclared member -->
<div>
The hero's marker is {{$any(hero).marker}}
</div>

// $any 转换函数可以和 this 联合使用,以便访问组件中未声明过的成员。

<!-- Accessing an undeclared member -->
<div>
Undeclared members is {{$any(this).member}}
</div>

用户输入

传入 $event 把整个 DOM 事件传到方法中,因为这样组件会知道太多模板的信息。这就违反了模板(用户看到的)和组件(应用如何处理用户数据)之间的分离关注原则。我们用模板引用变量来解决这个问题。

从一个模板引用变量中获得用户输入

1
2
3
4
5
6
7
8
@Component({
selector: 'app-loop-back',
template: `
<input #box (keyup)="0">
<p>{{box.value}}</p>
`
})
export class LoopbackComponent { }
  • 只有在应用做了些异步事件(如击键),Angular 才更新绑定(并最终影响到屏幕)。除非你绑定一个事件,否则这将完全无法工作。

生命周期钩子

生命周期钩子

通过 setter 截听输入属性值的变化

使用一个输入属性的 setter,以拦截父组件中值的变化,并采取行动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// trim掉名字中的空格

import { Component, Input } from '@angular/core';

@Component({
selector: 'app-name-child',
template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
private _name = '';

@Input()
set name(name: string) {
this._name = (name && name.trim()) || '<no name set>';
}

get name(): string { return this._name; }
}

通过ngOnChanges()来截听输入属性值的变化

当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。

父组件与子组件通过本地变量互动

父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CountdownTimerComponent是子组件
@Component({
selector: 'app-countdown-parent-lv',
template: `
<h3>Countdown to Liftoff (via local variable)</h3>
<button (click)="timer.start()">Start</button>
<button (click)="timer.stop()">Stop</button>
<div class="seconds">{{timer.seconds}}</div>
// #timer代表子组件。这样父组件的模板就得到了子组件的引用,于是可以在父组件的模板中访问子组件的所有属性和方法。
<app-countdown-timer #timer></app-countdown-timer>
`,
styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }

父组件调用@ViewChild()

这个本地变量方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。
如果父组件的类需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。
当父组件类需要这种访问时,可以把子组件作为ViewChild,注入到父组件里面。

1
2
3
4
5
6
7
export class CountdownViewChildParentComponent implements AfterViewInit {

@ViewChild(CountdownTimerComponent)
private timerComponent: CountdownTimerComponent;

// ...
}

父组件和子组件通过服务来通讯

父组件和它的子组件共享同一个服务,利用该服务在组件家族内部实现双向通讯。
该服务实例的作用域被限制在父组件和其子组件内。这个组件子树之外的组件将无法访问该服务或者与它们通讯。

组件样式

特殊的选择器

  • :host —— 用来选择组件宿主元素中的元素(相对于组件模板内部的元素)。
1
2
3
4
// 要把宿主样式作为条件,就要像函数一样把其它选择器放在 :host 后面的括号中。
:host(.active) {
border-width: 3px;
}
  • :host-context —— 它在当前组件宿主元素的祖先节点中查找 CSS 类, 直到文档的根节点为止。它也以类似 :host() 形式使用。

  • 已废弃 /deep/、>>> 和 ::ng-deep

视图封装模式

通过在组件的元数据上设置视图封装模式,你可以分别控制每个组件的封装模式。 可选的封装模式一共有如下几种:ShadowDom、Native、Emulated、None。

Angular 元素(Elements)概览

Angular 元素就是打包成自定义元素的 Angular 组件。所谓自定义元素就是一套与具体框架无关的用于定义新 HTML 元素的 Web 标准。
自定义元素扩展了 HTML,它允许你定义一个由 JavaScript 代码创建和控制的标签。 浏览器会维护一个自定义元素(也叫 Web Components)的注册表 CustomElementRegistry,它把一个可实例化的 JavaScript 类映射到 HTML 标签上。

// todo: 这一部分可以做一个实例验证之后再写。

动态组件

// todo: 这一章是动态广告 组件的实例。可以写一下验证之后再来补充。

属性型指令

在 Angular 中有三种类型的指令:

  • 组件 — 拥有模板的指令
  • 结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令。(NgIf)
  • 属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。(ngStyle)

编写属性型指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

// 属性型指令至少需要一个带有 @Directive 装饰器的控制器类。该装饰器指定了一个用于标识属性的选择器。 控制器类实现了指令需要的指令行为。

@Directive({
// 这里的方括号([])表示它的属性型选择器。 Angular 会在模板中定位每个拥有名叫 appHighlight 属性的元素,并且为这些元素加上本指令的逻辑。
selector: '[appHighlight]'
})
export class HighlightDirective {
// 你可以在指令的构造函数中使用 ElementRef 来注入宿主 DOM 元素的引用。
constructor(private el: ElementRef) { }

// 绑定到 @Input 别名
@Input('appHighlight') highlightColor: string;

// 响应用户引发的事件
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || 'red');
}
// ...

private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
1
2
// 使用属性型指令
<p appHighlight>Highlight me!</p>

结构型指令

结构型指令的职责是 HTML 布局。 它们塑造或重塑 DOM 的结构,比如添加、移除或维护这些元素。结构型指令非常容易识别,星号(*)被放在指令的属性名之前。
可以在一个宿主元素上应用多个属性型指令,但只能应用一个结构型指令。

NgIf

NgIf 会从 DOM 中移除它的宿主元素,而不是隐藏。

星号(*)前缀

星号是一个用来简化更复杂语法的“语法糖”。 从内部实现来说,Angular 把 *ngIf 属性 翻译成一个 元素 并用它来包裹宿主元素,代码如下:

1
2
3
4
5
<div *ngIf="hero" class="name">{{hero.name}}</div>
// 转化后
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>

微语法

Angular 微语法能让你通过简短的、友好的字符串来配置一个指令。 微语法解析器把这个字符串翻译成 上的属性。

// todo: 详细理解一下微语法

<ng-container>的应用

Angular 的 是一个分组元素,但它不会污染样式或元素布局,因为 Angular 压根不会把它放进 DOM 中。

<ng-container>的使用场景

有些 HTML 元素需要所有的直属下级都具有特定的类型。 比如,<select> 元素要求直属下级必须为 <option>,那就没办法把这些选项包装进 <div><span> 中。

1
2
3
4
5
6
7
8
9
10
11
12
// 下拉列表为空
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<span *ngFor="let h of heroes">
<span *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</span>
</span>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
// 下拉框工作正常
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<ng-container *ngFor="let h of heroes">
<ng-container *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</ng-container>
</ng-container>
</select>

管道

  • 你使用自定义管道的方式和内置管道完全相同。

  • 你必须在 AppModule 的 declarations 数组中包含这个管道。

  • 当使用管道时,Angular 会选用一种更简单、更快速的变更检测算法。

纯(pure)管道与非纯(impure)管道

1
2
3
4
5
//  默认情况下,管道都是纯的。以前见到的每个管道都是纯的。 通过把它的 pure 标志设置为 false,你可以制作一个非纯管道。
@Pipe({
name: 'flyingHeroesImpure',
pure: false
})
  • 纯管道 —— Angular只有在它检测到输入值发生了纯变更时才会执行纯管道。 纯变更是指对原始类型值(String、Number、Boolean、Symbol)的更改,或者对对象引用(Date、Array、Function、Object)的更改。会忽略(复合)对象内部的更改。(它保证了速度)

  • 非纯管道 —— Angular会在每个组件的变更检测周期中执行非纯管道。非纯管道可能会被调用很多次,和每个按键或每次鼠标移动一样频繁。

表单

Angular 表单检测

一般来说:

  • 响应式表单更健壮:它们的可扩展性、可复用性和可测试性更强。 如果表单是应用中的关键部分,或者你已经准备使用响应式编程模式来构建应用,请使用响应式表单。
  • 模板驱动表单在往应用中添加简单的表单时非常有用,比如邮件列表的登记表单。它们很容易添加到应用中,但是不像响应式表单那么容易扩展。如果你有非常基本的表单需求和简单到能用模板管理的逻辑,请使用模板驱动表单。

响应式表单

部分模型更新

有两种更新模型值的方式:

  • 使用 setValue() 方法来为单个控件设置新值。 setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值。
  • 使用 patchValue() 方法可以用对象中所定义的任何属性为表单模型进行替换。

修补(Patching)模型值

setValue() 方法的严格检查可以帮助你捕获复杂表单嵌套中的错误,而 patchValue() 在遇到那些错误时可能会默默的失败。

简单的表单验证

显示表单状态

你可以通过该 FormGroup 实例的 status 属性来访问其当前状态。

1
2
3
4
// 表单无效(验证不通过),值为INVALID
<p>
Form Status: {{ profileForm.status }}
</p>

使用表单数组管理动态控件

FormArray 是 FormGroup 之外的另一个选择,用于管理任意数量的匿名控件。像 FormGroup 实例一样,你也可以往 FormArray 中动态插入和移除控件,并且 FormArray 实例的值和验证状态也是根据它的子控件计算得来的。 不过,你不需要为每个控件定义一个名字作为 key,因此,如果你事先不知道子控件的数量,这就是一个很好的选择。

下面的例子展示了如何在 ProfileEditor 中管理一组绰号(aliases)。

  1. 定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { FormArray } from '@angular/forms';

// ...

profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
aliases: this.fb.array([
this.fb.control('')
])
});
  1. 访问
1
2
3
4
5
6
7
8
9
// 别名
// 相对于重复使用 profileForm.get() 方法获取每个实例的方式,getter 可以让你轻松访问表单数组各个实例中的别名。
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}

addAlias() {
this.aliases.push(this.fb.control(''));
}
  1. 在模板中显示表单数组
1
2
3
4
5
6
7
8
9
10
11
<div formArrayName="aliases">
<h3>Aliases</h3> <button (click)="addAlias()">Add Alias</button>

<div *ngFor="let address of aliases.controls; let i=index">
<!-- The repeated alias template -->
<label>
Alias:
<input type="text" [formControlName]="i">
</label>
</div>
</div>

模板驱动表单

创建表单组件

调试技巧,通过getter属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 你添加一个 diagnostic 属性,以返回这个模型的 JSON 形式。在开发过程中,它用于调试,最后清理时会丢弃它。

@Component({
selector: 'app-hero-form',
// ...
})
export class HeroFormComponent {

powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'];

model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');

submitted = false;

onSubmit() { this.submitted = true; }

// TODO: Remove this when we're done
get diagnostic() { return JSON.stringify(this.model); }
}

通过 ngModel 跟踪修改状态与有效性验证

NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。

状态 为真时的 CSS 类 为假时的 CSS 类
控件被访问过。 ng-touched ng-untouched
控件的值变化了。 ng-dirty ng-pristine
控件的值有效。 ng-valid ng-invalid

css例子

1
2
3
4
5
6
7
.ng-valid[required], .ng-valid.required  {
border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}

调试技巧,spy.className

1
2
3
4
5
6
// 往姓名 <input> 标签上添加名叫 spy 的临时模板引用变量, 然后用这个 spy 来显示它上面的所有 CSS 类。
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#spy>
<br>TODO: remove this: {{spy.className}}

验证器函数添加到模板驱动表单

在模板驱动表单中,你不用直接访问 FormControl 实例。所以不能像响应式表单中那样把验证器传进去,而应该在模板中添加一个指令。

  1. 自定义验证函数(略)

  2. 指令

1
2
3
4
5
6
7
8
9
10
11
12
@Directive({
selector: '[appForbiddenName]',
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
@Input('appForbiddenName') forbiddenName: string;

validate(control: AbstractControl): {[key: string]: any} | null {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
: null;
}
}
  1. 模板中使用
1
2
3
<input id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" >

异步验证

异步验证总是会在同步验证之后执行,并且只有当同步验证成功了之后才会执行。如果更基本的验证方法已经失败了,那么这能让表单避免进行可能会很昂贵的异步验证过程,比如 HTTP 请求。

例子: 在异步验证结束前会pending

1
2
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner>
1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable({ providedIn: 'root' })
export class UniqueAlterEgoValidator implements AsyncValidator {
constructor(private heroesService: HeroesService) {}

validate(
ctrl: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
catchError(() => null)
);
}
}
1
2
3
interface HeroesService {
isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}

性能问题

默认情况下,每当表单值变化之后,都会执行所有验证器。对于同步验证器,没有什么会显著影响应用性能的地方。不过,异步验证器通常会执行某种 HTTP 请求来对控件进行验证。如果在每次按键之后都发出 HTTP 请求会给后端 API 带来沉重的负担,应该尽量避免。

我们可以把 updateOn 属性从 change(默认值)改成 submit 或 blur 来推迟表单验证的更新时机。

1
2
3
4
5
6
7
// 对于模板驱动表单:

<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">

// 对于响应式表单:

new FormControl('', {updateOn: 'blur'});

NgModule

服务提供商

服务提供商作用域

providedIn 与 NgModule

1
2
3
4
5
6
7
8
9
// 也可以规定某个服务只有在特定的 @NgModule 中提供。比如,如果你你希望只有当消费方导入了你创建的 UserModule 时才让 UserService 在应用中生效,那就可以指定该服务要在该模块中提供:
import { Injectable } from '@angular/core';
import { UserModule } from './user.module';

@Injectable({
providedIn: UserModule,
})
export class UserService {
}

上面的例子展示的就是在模块中提供服务的首选方式。之所以推荐该方式,是因为当没有人注入它时,该服务就可以被摇树优化掉。
如果没办法指定哪个模块该提供这个服务,你也可以在那个模块中为该服务声明一个提供商:

1
2
3
4
5
6
7
8
9
import { NgModule } from '@angular/core';

import { UserService } from './user.service';

@NgModule({
providers: [UserService],
})
export class UserModule {
}

单例服务

提供单例服务

在 Angular 中有两种方式来生成单例服务:

  • 声明该服务应该在应用的根上提供。
  • 把该服务包含在 AppModule 或某个只会被 AppModule 导入的模块中。

forRoot()

如果某个模块同时提供了服务提供商和可声明对象(组件、指令、管道),那么当在某个子注入器中加载它的时候(比如路由),就会生成多个该服务提供商的实例。 而存在多个实例会导致一些问题,因为这些实例会屏蔽掉根注入器中该服务提供商的实例,而它的本意可能是作为单例对象使用的。

因此,Angular 提供了一种方式来把服务提供商从该模块中分离出来,以便该模块既可以带着 providers 被根模块导入,也可以不带 providers 被子模块导入。

在该模块上创建一个静态方法 forRoot()(习惯名称)。

把那些服务提供商放进 forRoot 方法中,参见下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// service
constructor(@Optional() config: UserServiceConfig) {
if (config) { this._userName = config.userName; }
}

// coremodule
static forRoot(config: UserServiceConfig): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}

// app.module
import { CoreModule } from './core/core.module';
/* . . . */
@NgModule({
imports: [
...
CoreModule.forRoot({userName: 'Miss Marple'}),
],
/* . . . */
})
export class AppModule { }

防止重复导入 CoreModule

1
2
3
4
5
6
7
8
9
10
// coremodule

// 这个构造函数要求 Angular 把 CoreModule 注入到它自己。 如果 Angular 在当前注入器中查找 CoreModule,这个注入过程就会陷入死循环。 而 @SkipSelf 装饰器表示 “在注入器树中那些高于我的祖先注入器中查找 CoreModule”。

constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded. Import it in the AppModule only');
}
}

依赖注入

Angular注入依赖

那些需要其它服务的服务

可选依赖,@Optional

1
2
3
4
5
6
7
import { Optional } from '@angular/core';
// ...
constructor(@Optional() private logger: Logger) {
if (this.logger) {
this.logger.log(some_message);
}
}

依赖提供商

Provider 对象字面量

1
2
3
4
providers: [Logger]
// <=>
// 第二个属性是一个提供商定义对象,它告诉注入器要如何创建依赖值。 提供商定义对象中的 key 可以是 useClass —— 就像这个例子中一样。 也可以是 useExisting、useValue 或 useFactory。
[{ provide: Logger, useClass: Logger }]

代替类提供商

带依赖的类提供商

1
2
3
4
5
6
7
8
9
// service
@Injectable()
export class EvenBetterLogger extends Logger {
// ...
}

// module
[ UserService,
{ provide: Logger, useClass: EvenBetterLogger }]

别名类提供商

1
2
3
4
5
6
7
8
9
10
11
// 如果你试图用 useClass 为 OldLogger 指定一个别名 NewLogger,就会在应用中得到 NewLogger 的两个不同的实例。

[ NewLogger,
// Not aliased! Creates two instances of `NewLogger`
{ provide: OldLogger, useClass: NewLogger}]

// 要确保只有一个 NewLogger 实例,就要用 useExisting 来为 OldLogger 指定别名。

[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]

值提供商

1
[{ provide: Logger, useValue: silentLogger }]

非类依赖

  1. 注入字符串、函数或对象。
1
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
  1. 另一个为非类依赖选择提供商令牌的解决方案是定义并使用 InjectionToken 对象。
1
2
3
4
5
6
7
8
9
10
//
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

//
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

//
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}

工厂提供商

有时候你需要动态创建依赖值,创建时需要的信息你要等运行期间才能拿到。 比如,你可能需要某个在浏览器会话过程中会被反复修改的信息,而且这个可注入服务还不能独立访问这个信息的源头。

1
2
3
4
5
export let heroServiceProvider =
{ provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
};

HttpClient

获取 JSON 数据

  • 带类型检查的响应
1
2
3
4
getConfig() {
// now returns an Observable of Config
return this.http.get<Config>(this.configUrl);
}
  • 读取完整的响应体
1
2
3
4
5
// 你就要通过 observe 选项来告诉 HttpClient,你想要完整的响应信息,而不是只有响应体
getConfigResponse(): Observable<HttpResponse<Config>> {
return this.http.get<Config>(
this.configUrl, { observe: 'response' });
}

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误处理器

private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an observable with a user-facing error message
return throwError(
'Something bad happened; please try again later.');
};
1
2
3
4
5
6
getConfig() {
return this.http.get<Config>(this.configUrl)
.pipe(
catchError(this.handleError)
);
}

高级用法

请求的防抖(debounce)

1
2
3
4
5
6
7
8
9
10
// 如果每次击键都发送一次请求就太昂贵了。 最好能等到用户停止输入时才发送请求。

<input (keyup)="search($event.target.value)" id="name" placeholder="Search"/>

<ul>
<li *ngFor="let package of packages$ | async">
<b>{{package.name}} v.{{package.version}}</b> -
<i>{{package.description}}</i>
</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
withRefresh = false;
packages$: Observable<NpmPackageInfo[]>;
private searchText$ = new Subject<string>();

search(packageName: string) {
this.searchText$.next(packageName);
}

ngOnInit() {
this.packages$ = this.searchText$.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(packageName =>
this.searchService.search(packageName, this.withRefresh))
);
}

constructor(private searchService: PackageSearchService) { }

拦截请求和响应

  1. 编写拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { Observable } from 'rxjs';

/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {

intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req);
}
}
  1. 提供拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
// 简单
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },

// 可提供多个
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { NoopInterceptor } from './noop-interceptor';

/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];

拦截器顺序

Angular 会按照你提供它们的顺序应用这些拦截器。 如果你提供拦截器的顺序是先 A,再 B,再 C,那么请求阶段的执行顺序就是 A->B->C,而响应阶段的执行顺序则是 C->B->A。

小结

基本上比较细致的浏览了一遍angular官网上的内容,记录了一些在平时的运用中较少接触到的内容。至于RxJS,animation,i18n,库的开发,可以看作比较独立的内容,再此就不多赘述了。