组件
在 Angular 中,组件是用来定义视图的,一个应用至少有一个根组件(属于根模块的一部分);组件由三部分组成:组件类(控制组件模板渲染)、组件模板、组件样式(默认仅对当前组件生效,不会污染全局);可以使用 ng generate 命令或
编辑器扩展
快速新建组件(服务、管道…):
- 组件模板,语法和 HTML 几乎相同,多了一些 Angular 独有的指令和标签;
- 组件样式,作用域仅限于当前组件(可以通过“样式封装策略 encapsulation”进行更改);
- 组件类,就是普通的 class;组件类中声明的属性和方法会自动注入组件模板中。
组件的父子关系是通过“组件的视图对象”间接建立的;每个组件都有一个宿主视图和一些内建视图。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
const slideMotion = trigger('slideMotion', [
// 创建一组有名字的 CSS 样式,它会在成功转换到指定的状态时应用到元素上
state(
'void',
// 定义一个或多个要用于动画中的 CSS 样式
style({
opacity: 0,
transform: 'scaleY(0.8)'
})
),
state(
'enter',
style({
opacity: 1,
transform: 'scaleY(1)'
})
),
// 定义两个命名状态之间的动画序列
transition('void => *', [animate('0.2s cubic-bezier(0.7, 0.3, 0.1, 1)')]),
transition('* => void', [animate('0.2s cubic-bezier(0.86, 0, 0.07, 1)')])
]);
// 组件类需要由 @Component 装饰器进行修饰
@Component({
// 动画相关元数据,用来放置“定义动画的触发器”
animations: [slideMotion],
/**
* 变更检测策略:
* 1. ChangeDetectionStrategy.Default:异步逻辑结束(事件、请求等等),NgZone 会自上而下对整个组件树做变更检测
* 2. ChangeDetectionStrategy.OnPush:用来跳过某些变更检测(因为不是每个操作都需要进行变更检测,React 也有类似的机制)
* 在 OnPush 策略下,有四种情况仍旧可以触发变更检测:
* 1. 组件 @Input 引用变化(这种情况下,搭配 Immutable.js 使用更佳)
* 2. 组件及其子组件的 DOM 事件触发(计时器、Promise、请求均不会触发检测)
* 3. 组件内 Observable 订阅事件,同时设置 AsyncPipe
* 4. 手动调用:
* ChangeDetectorRef.detectChanges(),立即触发组件的变更检测
* ChangeDetectorRef.markForCheck(),等待应用的下一轮变更检测
* ApplicationRef.tick(),触发整个应用的变更检测
*/
changeDetection: ChangeDetectionStrategy.Default,
/**
* 样式封装策略:
* 1. ViewEncapsulation.ShadowDom,样式仅添加到 ShadowDOM 宿主中,基于 ShadowDOM 实现组件样式的封装与隔离;
* 2. ViewEncapsulation.Emulated,默认策略,样式会被添加到 <head> 中,基于 CSS 属性选择器来实现组件样式的封装与隔离;
* 3. ViewEncapsulation.None,样式会被添加到 <head> 中,不对样式进行封装与隔离,样式作用于全局(document)。
* ViewEncapsulation.ShadowDom 不是默认策略的原因是“并不是每个浏览器都支持 ShadowDOM”
*/
encapsulation: ViewEncapsulation.Emulated,
/**
* 放置一些『被动态调用』的组件,这些组件没有“直接”在模板中使用,Ng 是不知道是否应该对这些组件进行编译的
* entryComponents 引入的组件会通过 ViewContainerRef.createComponent 方法被动态调用
* 编译器会将这些组件编译并为他们创建“工厂函数”
* 比如,一些在 Modal 中展示的组件
*/
entryComponents: [],
// 改写默认的插值表达式起止分界符,在和第三方库冲突的时候才会修改
interpolation: [],
// 仅在 CommonJS 规范下有意义,用于解析组件模板和样式
moduleId: '',
// 是否从编译后的模板中移除多余的空白字符
preserveWhitespaces: false,
/**
* 组件“在模版中”只能以 <app-example></app-example> 的方式进行调用
* selector 决定了组件在模版中的调用方式
* 当 Angular 在模版中找到相应的标签时
* 就把该组件实例化在那里
* 可选属性
*/
selector: 'app-example',
// 组件“在模版中”只能以 <xxx [app-example]></xxx> 的方式进行调用
selector: '[app-example]',
// 内部样式字符串
styles: 'div{...} div>p{...}',
// 外部样式路径
styleUrls: ['./app.component.scss'],
/**
* 内部模板字符串
* template 和 templateUrl 不能同时存在
*/
template: '<div><p>...</p></div>',
// 外部模板路径
templateUrl: './app.component.html',
// viewProviders 注册的 provider 只对 viewChildren 可见,对 contentChildren 不可见
viewProviders: [BbService],
/**
* providers 注册的 provider 对 contentChildren 和 viewChildren 均可见
* 如果你愿意的话,可以总是使用 providers 而忽略 viewProviders
*/
providers: [AaService]
})
class ExampleComponent
implements
OnChanges,
DoCheck,
OnDestroy,
OnInit,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked
{
private temp;
/**
* 使用 @Input 修饰组件要从外部接受的属性
* <app-example [propOne]="someValue"></app-example>
*/
@Input() propOne!: 'defaultPropOneValue';
/**
* 可以给 propTwo 起一个别名(风格指南并不推荐)
* <app-example [prop-two]="someValue"></app-example>
* 在组件内部,仍可以用 this.propTwo 访问接受到的属性值
*/
@Input('prop-two') propTwo!: 'defaultPropTwoValue';
/**
* 可以把 propThree 写成 getter/setter
* 来“监听”外部的值的变化
*/
@Input('prop-three')
get propThree() {
return this.temp;
}
set propThree(value) {
// 可以执行一些别的逻辑
this.temp = value;
}
/**
* 使用 @Output 修饰组件上绑定的自定义事件
* 通过 this.updatePropOne.emit(newValue) 向外传值($event 会接收到 newValue)
* <app-example (updatePropOneAlias)="handleXXX($event)"></app-example>
* EventEmitter 继承了 RxJS 中的 Subject
* 在“服务”中不能使用 EventEmitter,可以使用 Subject 代替!!!
* export declare interface EventEmitter<T> extends Subject<T> {...}
*/
@Output('updatePropOneAlias') updatePropOne = new EventEmitter<any>();
/**
* Angular 不会接管构造函数
* 在构建组件树的时候会被调用
* 此时,还不能访问组件 DOM(ngOnInit 可以访问 DOM)
* 组件初始化需要在生命周期函数中进行
* 这里仅仅用来注入一些组件需要依赖的服务
* 『依赖注入』机制会分析构造函数的『形参』
* 调用时,已经处理好根/父级 injectors 了
* 在 class 实例化的过程中,会向上查找 providers
* 并将查找的结果传递回构造函数,在构造完成之后才会设置指令的数据绑定输入属性
*/
constructor(
/**
* conastructor 是在 injector 上下文中调用的唯一方法
* Angular 的 DI 系统会自动帮我们实例化注入的 Service
* 并将服务实例绑定到当前实例上(也就是 this)
* 方便我们以 this.xxService 的方式调用
*/
private aaService: AaService,
private bbService: BbService,
private ccService: CcService
) {}
/**
* Angular 的运行机制分为两部分:
* 1. 构建组件树;(调用 contructor)
* 2. 执行脏检查。(调用生命周期函数)
* 生命周期钩子(属于变更检测的一部分,起始于组件实例化,结束于组件实例的销毁)的执行顺序:
* 1. ngOnChanges(可能执行多次,需注意逻辑精简,仅在有 @Input 属性的情况下)
* 2. ngOnInit
* 3. ngDoCheck(可能执行多次,需注意逻辑精简)
* 4. ngAfterContentInit
* 5. ngAfterContentChecked(可能执行多次,需注意逻辑精简)
* 6. ngAfterViewInit(仅存在组件,不能用于指令)
* 7. ngAfterViewChecked(可能执行多次,需注意逻辑精简;仅存在组件,不能用于指令)
* 8. ngOnDestroy
* Compiler 会检查组件的钩子函数,给组件赋予不同的 NodeFlags
* 在“脏检查”阶段,Angular 会根据组件的 NodeFlags 来调用对应的 钩子
*/
ngOnChanges(
// changes 仅包含了每个『发生变化』的属性
changes: SimpleChanges
): void {
/**
* 1. 如果组件绑定过输入属性,所绑定的一个或多个输入属性的值发生变化时都会调用
* 2. 如果绑定的是一个对象,如果对象的引用没变,ngOnChanges 就不会执行
* 3. 如果你的组件『没有输入属性』,框架就『不会调用该方法』
* 4. 在 ngOnInit 之前调用
*/
for (const propName in changes) {
const { currentValue, previousValue, firstChange } = changes[propName];
console.log(
`current: ${currentValue}, previous: ${previousValue}, isFirstChange: ${firstChange}`
);
}
if ('propThree' in changes) {
// 除了 getter/setter,也可以酱紫“监听”变化
}
}
ngOnInit(): void {
/**
* 在 ngOnChanges 第一次完成调用之后执行(即使没有绑定任何输入属性)
* 用来执行复杂的在构造函数之外的组件初始化逻辑
* 获取初始数据的好地方
*/
this.route.queryParams.subscribe(params => {
// 每当路由的查询参数变化,都重新获取数据
this.fetchData(params);
});
}
ngDoCheck(): void {
/**
* 用于检测 Angular 无法捕获的变更
* 紧跟在每次执行变更检测时的 ngOnChanges 和首次执行变更检测时的 ngOnInit 后调用
*/
}
ngAfterContentInit(): void {
/**
* 当 Angular 把外部内容投影进组件视图或指令所在的视图之后调用
* ngDoCheck 首次调用之后调用
* 仅仅调用一次
*/
}
ngAfterContentChecked(): void {
/**
* 每当 Angular 把外部内容投影进组件视图或指令所在的视图之后调用
* ngAfterContentInit 每次调用之后调用
* ngDoCheck 每次调用之后调用
*/
}
ngAfterViewInit(): void {
/**
* 当 Angular 初始化完组件视图及其子视图或包含该指令的视图之后调用
* ngAfterContentChecked 首次调用之后调用
* 仅仅调用一次
*/
}
ngAfterViewChecked(): void {
/**
* 每当 Angular 做完组件视图和子视图或包含该指令的视图的变更检测之后调用
* ngAfterContentChecked 每次调用之后调用
* ngAfterViewInit 每次调用之后调用
*/
}
ngOnDestroy(): void {
/**
* 每当指令/组件销毁之前立即调用,用于释放不会被垃圾回收的资源:
* 1. 反注册在应用服务中注册的回调
* 2. 取消订阅可观察对象
* 3. 移除事件绑定
* 4. 清除计时器
*/
}
}
|
指令(没有“模版”的特殊组件)
指令用来将自定义行为附加到 DOM 中:
- 结构性指令:通过添加、移除、替换 DOM 来修改布局,例如:*ngIf、*ngFor、*ngSwitch、*ngSwitchDefault、*ngComponentOutlet、*ngTemplateOutlet;
- 属性型指令:改变已有元素的外观或行为,例如:ngClass、ngStyle、ngModel、ngNonBindable、ngPreserveWhitespaces;
- Angular 没有提供 AngularJS 中的 ng-show 指令。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
@Directive({
/**
* <div [app-example] #exampleRef="example"></div>
* 可以通过 exampleRef 获取到“当前指令的引用”
*/
exportAs: 'example',
// 将类的属性映射到宿主元素的绑定
host: {
/**
* 当宿主元素(绑定的元素)上的 click 事件被触发时
* 调用当前指令内的 onClick 方法
*/
'(click)': 'onClick()',
// 宿主元素的 class 被设置为 class-name-example(this.innerClass 的值)
'[class]': 'innerClass'
},
// 可以替代 @Input 装饰器
inputs: [],
// 可以替代 @Output 装饰器
outputs: [],
/**
* 指令代码会被 AOT(ahead-of-time)编译器忽略
* 也就是说相关代码不会被预编译(打包的时候编译)
* 而是保留到运行时进行动态编译
*/
jit: true,
// 依赖注入,详见“组件”
providers: [],
/**
* 用来标记在哪些元素/组件上使用指令
* <div [app-example]></div>
*/
selector: '[app-example]'
})
class ExampleDirective {
// 也可以通过 @HostBinding 向宿主元素“绑定属性”
@HostBinding('class') class = 'class-name';
// 也可以通过 @HostListener 向宿主元素“绑定事件”
@HostListener('input', ['$event']) onInput(event) {
console.log(event);
}
constructor(el: ElementRef, render2: Renderer2) {
// 通过 ElementRef 可以拿到“宿主元素”的引用
el.nativeElement.style.backgroundColor = 'red';
}
innerClass: string = 'class-name-example';
onClick() {
console.log('宿主元素上触发了 click 事件');
// 借助于 Renderer2 可以修改 Angular 的默认渲染行为
this.render2.setStyle(this.el.nativeElement, 'color', 'red');
}
}
|
结构型指令的实现需要借助辅助类 TemplateRef 和 ViewContainerRef:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 一个“延迟渲染”的例子
@Directive({
selector: '[delayRender]'
})
export class DelayRender {
constructor(
private templateRef: TemplateRef<any>, // 对 ng-template 的引用
private viewContainerRef: ViewContainerRef // 对容器 View 的引用
) {}
@Input()
set delayRender(timer: number) {
this.viewContainerRef.clear();
setTimeout(() => {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}, timer);
}
}
|
管道
Angular 内置了 async(从 Promise/Observable 中取值)、date、currency、json(JSON2String)、uppercase、lowercase、titlecase、precent 等管道。
无论是用法还是效果都和 Vue 3.0 中已经删除的 filter 很像,用于数据转换。
有一点需要注意:管道操作符的优先级高于逻辑运算符。
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
34
35
36
37
38
39
40
41
42
|
type NzSafeAny = any;
type DomSanitizerType = 'html' | 'style' | 'js' | 'url' | 'resourceUrl';
@Pipe({
name: 'sanitizer', // 管道名称
/**
* 是否为“纯”管道:
* 1. “纯”管道只有在原始数据/对象的"引用变更"时才会执行;
* 2. “纯”管道会"忽略引用类型内部属性"的变化(引用没变);
* 3. “纯”管道内部的的 transform 方法必须为“纯”函数;
* 4. “非纯”管道性能较差(任何交互行为都会触发执行)。
*/
pure: true
})
export class SanitizerPipe implements PipeTransform {
constructor(protected sanitizer: DomSanitizer) {}
transform(value: NzSafeAny, type: 'html'): SafeHtml;
transform(value: NzSafeAny, type: 'style'): SafeStyle;
transform(value: NzSafeAny, type: 'js'): SafeScript;
transform(value: NzSafeAny, type: 'url'): SafeUrl;
transform(value: NzSafeAny, type: 'resourceUrl'): SafeResourceUrl;
transform(
value: NzSafeAny,
type: DomSanitizerType = 'html'
): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
switch (type) {
case 'html': // 绕过安全检查,并信任给定的值是一个安全的 HTML
return this.sanitizer.bypassSecurityTrustHtml(value);
case 'style': // 绕过安全检查,并信任给定的值是一个安全的 CSS
return this.sanitizer.bypassSecurityTrustStyle(value);
case 'js': // 绕过安全检查,并信任给定的值是一个安全的 JS
return this.sanitizer.bypassSecurityTrustScript(value);
case 'url': // 绕过安全检查,并信任给定的值是一个安全的样式 URL(可以安全的用在 <img src> 中)
return this.sanitizer.bypassSecurityTrustUrl(value);
case 'resourceUrl': // 绕过安全检查,并信任给定的值是一个安全的资源 URL(可以安全的用在 <script src>、<iframe src> 中)
return this.sanitizer.bypassSecurityTrustResourceUrl(value);
default:
throw new Error(`指定的安全类型无效`);
}
}
}
|
通常我们会在 ngOnInit 中的进行数据请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class TicketListComponent implements OnInit {
data: Array<{
id: number;
title: srting;
}>;
constructor(private route: ActivatedRoute, private apiService: ApiService) {}
ngOnInit() {
this.apiService.fetchTickets1().subscribe(list => (this.data = list || []));
// 或者
this.route.queryParams.subscribe(params => {
this.apiService
.fetchTickets2(params.id)
.subscribe(list => (this.data = list || []));
});
}
}
|
AsyncPipe 可以替代 ngOnInit 完成这部分逻辑:
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
34
|
interface Seller {
id: number;
name: srting;
}
interface Ticket {
id: number;
title: string;
}
@Component({
template: `
<p>
<span>{{ (firstSeller$ | async).name }}</span>
</p>
// 借助 as 将其重命名为 list
<ul *ngIf="tickets$ | async as list">
<li *ngFor="let item of list">{{ item.title | emptyValue }}</li>
</ul>
`
})
class SellerComponent {
// 借助 pipe + map 对数据进行处理
readonly firstSeller$: Seller = this.apiService.fetchSellers().pipe(
map(list => {
const { id, name } = list[0];
return { id, name };
})
);
// 借助 switchMap 获取路由参数
readonly tickets$: Ticket = this.route.queryParams.pipe(
switchMap(params => this.apiService.fetchTickets(params.id))
);
constructor(private route: ActivatedRoute, private apiService: ApiService) {}
}
|
如果不喜欢模版中出现太多 async,可以借助 combineLatest 将多个 Observable 合并:
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
|
@Component({
template: `
<ng-container *ngIf="vm$ | async as vm">
<ul>
<li *ngFor="let item of vm.boys">{{ item.name }}</li>
</ul>
<ul>
<li *ngFor="let item of vm.girls">{{ item.name }}</li>
</ul>
<ul>
<li *ngFor="let item of vm.dogs">{{ item.nickName }}</li>
</ul>
<ul>
<li *ngFor="let item of vm.cats">{{ item.nickName }}</li>
</ul>
</ng-container>
`
})
class BuyerComponent {
readonly vm$ = combineLatest([
this.route.queryParams.pipe(
switchMap(params => this.apiService.fetchBoys(params.id))
),
this.route.queryParams.pipe(
switchMap(params => this.apiService.fetchGirls(params.id))
),
this.apiService.fetchDogs(),
this.apiService.fetchCats()
]).pipe(map(([boys, girls, dogs, cats]) => ({ boys, girls, dogs, cats })));
constructor(private route: ActivatedRoute, private apiService: ApiService) {}
}
|
自从 Angular 的编译和渲染引擎切换到 Ivy 之后:
- ChangeDetectionStrategy.Default 策略下,在每次“变更检测”的过程中,“纯”管道的 transform 方法,只有在入参发生变化的情况下才会被调用;
- ChangeDetectionStrategy.Default 策略下,在每次“变更检测”的过程中,“非纯”管道的 transform 方法都会被调用;
- 当使用 async 管道时(它是“非纯”的),最好使用 ChangeDetectionStrategy.OnPush 策略;
- 无论是否是“纯”管道,每个管道都会有自己的实例(View Engine 时期,“纯”管道是共享实例的);
- 在同一模版中,使用了两次 A 管道,则会有两个 A 管道的实例存在。
服务
服务是 Angular 的核心,他是专门用来处理某件事情的(数据请求等)、具有明确定义的 class;一般情况下,服务用来创建与视图无关但需要跨组件共享的数据/逻辑。
官方推崇“Component 处理页面展示,Service 处理内在逻辑”的原则(有点像 React 中的简单组件和逻辑组件)。
服务一般(具体看 providedIn 的取值)以单例的形式存在,实例化之后,应用于整个应用。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
@Injectable({
/**
* providedIn 有四种取值:
* 1. platform:平台级别注入,所有应用共享一个服务单例(仅适用于在一个 window 下运行多个 Angular 应用,有点微前端的意思);
* 2. root:应用程序(AppModule)/系统/全局/根级别注入,服务可以在整个应用中使用,而且共享同一个服务实例;
* 3. [NgModule]:仅注入指定的 NgModule 中,仅可在指定的 NgModule 中使用,有 TreeShaking 加成;
* 4. any:在每个注入的模块中,提供一个唯一的实例;
* 5. null:仅仅表明可注入。
*/
providedIn: 'root'
})
class ApiPublicService {
// 服务也可依赖别的服务,这里就依赖了 HttpClient
constructor(private http: HttpClient) {}
private paginationData(data: any): PaginationData {
return {
total: data.total,
pageSize: data.size,
pageIndex: data.current
};
}
private buildResponse(response) {
const temp = {
success: response.success,
status: response.status,
data: response.data,
raw: response,
records: response?.data?.records || [],
objects: response?.data || {}
};
if (response.errorCode) {
temp.errorCode = response.errorCode;
}
if (response.errorMessage) {
temp.errorMessage = response.errorMessage;
}
return temp;
}
private toResponse() {
return pipe(
map(response => this.buildResponse(response)),
tap(response => {
if (response.errorCode === 'LOGIN_INVALID') {
// ...
} else if (!response.success) {
// ...
}
})
);
}
public fetchList(params: ApiParams) {
return this.http
.get(`${environment.apiHost}/api/v1/list`, {
params
})
.pipe(
this.toResponse(),
map(response => ({
paginationData: this.paginationData(response.data),
records: response.data.records,
pages: response.data.pages
}))
);
}
}
|
模块
NgModule 是 Angular 的基本构造块(NgModule 和 ESModule 不同但互补),也是 Angular 对“依赖”进行组织的一种形式;其为组件提供了编译上下文;一个 Angular 应用由至少一个根模块(AppModule)和多个特性模块组成。
NgModule 可以将组件及其相关代码(服务等)关联,形成功能单元(一般是按照业务划分的);模块之间即相互独立又可通过 imports/exports 建立联系;模块可以让代码更“内聚”。
一个 Angular 应用一般包含四种模块:
- 根模块(引导模块),一个应用仅有一个根模块;
- 核心模块,为应用提供基础能力的模块,比如:路由模块、数据请求模块;
- 公共模块,包含了:公共组件、公共管道、公共指令;
- 业务模块,封装了具体业务的模块。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
@NgModule({
/**
* 使用:platformBrowserDynamic().bootstrapModule(AppModule) 启动根模块
* 只有根模块(一个应用只有一个)才设置这个属性
* 表示该模块应用的主视图(一般是根组件)
* 同时也表明这个模块是“根模块”
*/
bootstrap: [AppComponent],
/**
* 声明该模块(包含/定义)的组件、管道、指令
* 这些组件、管道、指令是当前模块“私有的”(如果没有放入 exports)
* 一个组件/指令/管道只能属于一个模块,不能同时把一个类声明在几个模块中
*/
declarations: [ExampleComponent, ExamplePipe, ExampleDirective],
/**
* 如果组件没有直接在模版中使用,AOT 就不会提前编译组件
* 如果将组件添加到 entryComponents
* AOT 就会提前编译组件
*/
entryComponents: [],
/**
* 当前 NgModule 在 getNgModuleById 中的名字或唯一标识符
* 如果为 undefined,则该模块不会被注册到 getNgModuleById 中
* 用到的不多,官网也没做过多介绍,Angular 会自动帮我们处理
*/
id: undefined,
/**
* 导入(当前模块运行需要依赖的的)其他模块:
* 1. 被导入模块的 exports(低优先级)会和当前模块的 declarations(高优先级)合并
* 2. 被导入模块的 providers 会和当前模块的 providers 合并
* 3. 被导入模块的路由会和当前模块的路由合并
*/
imports: [BrowserModule, OneModule, TwoModule],
// 暴露(可供其他模块使用的)当前模块中的一些组件、指令和管道
exports: [],
// 编译器选项,详见“指令”
jit: true,
/**
* 当前模块需要的提供者(可注入对象,例如:ApiService)
* 如果 provider 在根模块注入,则注入的 provider 为单例模式
*/
providers: [],
/**
* 当前 NgModule 中允许使用的声明元素的 HTML 架构
* 当遇到不认识的元素时,怎么做处理
* 用于测试代码,业务中用不上
*/
schemas: []
})
class ExampleModule {}
|
表单
Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单和模板驱动表单。两者都从视图中捕获用户输入事件、验证用户输入、创建表单模型、修改数据模型,并提供跟踪这些更改的途径:
- 响应式表单:提供对底层表单对象模型直接、显式的访问;可伸缩性、可扩展性、可复用性和可测试性都更高;视图和数据模型之间使用同步数据流;
- 模版驱动表单:依赖模板中的指令来创建和操作底层的对象模型,在扩展性方面不如响应式表单,专注于简单场景,视图和数据模型之间使用异步数据流。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
/**
* 使用“响应式表单”,需要在 NgModule 中导入 ReactiveFormsModule 和 FormsModule:
* ReactiveFormsModule:提供 formControl、ngFormGroup 等指令;
* FormsModule:提供 ngForm、ngModel 等模版驱动指令。
* ngForm 的 selector 包含了 form:not([formGroup]) ;
* 所以,一般不用显式添加 ngForm 指令。
*/
import {
FormBuilder, // 提供一种更方便使用 FormGroup 和 FormControl 的方式
FormControl, // 保存了字段的值和状态(是否合法、是否修改、是否禁用等)
FormGroup, // 用来创建和管理一组 FormControl
Validators
} from '@angular/forms';
/**
* submit 与 ngSubmit 的区别:
* submit 是 DOM 事件,ngSubmit 是 Angular 事件;
* 阻止默认行为需要在 submit 中进行;
* ngSubmit 比 submit 先触发;
* 两者的事件对象是一样的。
*/
@Component({
selector: 'app-form',
template: `
<form [formGroup]="validateForm" (ngSubmit)="handleSubmit($event)">
<label for="name">
<span>Name:</span>
<input id="name" name="name" type="text" formControlName="name" />
</label>
<label for="age">
<span>Age:</span>
<input id="age" name="age" type="text" formControlName="age" />
</label>
<div formGroupName="address" alt="嵌套表单组">
<label for="street">
<span>Street:</span>
<input
id="street"
name="street"
type="text"
formControlName="street"
/>
</label>
<label for="city">
<span>City:</span>
<input id="city" name="city" type="text" formControlName="city" />
</label>
<label for="state">
<span>State:</span>
<input id="state" name="state" type="text" formControlName="state" />
</label>
</div>
<button (click)="handleFormUpdate()">Update</button>
<button type="submit">Submit</button>
</form>
`
})
class FormComponent implements OnInit {
validateForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
/**
* 一般使用这种,并且对于“动态表单”,最好在初始化的时候就将所有表单属性列出
* 而不是根据条件来动态的 addControl/removeControl
* 因为那样,整个表单会非常不直观和零散
*/
this.validateForm = this.fb.group({
name: ['', Validators.required],
age: ['18', Validators.required],
address: this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required],
state: ['', Validators.required]
})
});
// 很少使用这种
this.validateForm = new FormGroup({
name: new FormControl(''),
age: new FormControl('18'),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl('')
})
});
{
/**
* 表单不仅可以是一个对象,而且可以是一个数组
* 不过在表单验证的时候,可能需要类型断言
*/
this.exampleForm = this.formBuilder.array([]);
/**
* 通过 push 向数组表单中添加表单项
* 还有 insert、removeAt 等方法
*/
this.exampleForm.push(
this.formBuilder.group({
type: ['SELF_EMPLOYED', []],
accountName: [accountName, [Validators.required]],
accountNo: [accountNo, [Validators.required]],
bankName: [bankName, [Validators.required]],
bankBranchName: [bankBranchName, [Validators.required]]
})
);
}
{
this.validateForm.valueChanges.subscribe(form => {
// 可以通过 valueChanges 来监听表单变化
});
}
}
handleSetDefault() {
// 单个赋值
this.validateForm.get('name').setValue('Bob');
this.validateForm.get('address').get('street').setValue('Gongshu');
// 批量赋值
this.validateForm.patchValue({
gender: 'male', // 不会抛出错误
age: '21',
address: {
city: 'Zhejiang'
}
});
}
handleSubmit($event) {
$event.preventDefault();
if (this.validateForm.status === 'VALID') {
console.log(this.validateForm.value);
}
}
}
|
路由
Router 是一个提供导航和操纵 URL 能力的 NgModule;通过将浏览器 URL 解释为更改视图的操作指令来启用导航:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
interface Route {
canActivate?: any[]; // 调用 canActivate 钩子来确定“是否激活匹配到的路由”
canActivateChild?: any[]; // 调用 canActivateChild 钩子来确定“路径参数的变化时,是否激活匹配到的子路由”
canDeactivate?: any[]; // 调用 canActivate 钩子来确定“提交表单时,是否允许离开当前路由”
canLoad?: any[]; // 调用 canLoad 钩子来确定“是否允许加载‘懒加载’的模块”
children?: Route[]; // 嵌套子路由配置
component?: Type<any>; // 匹配之后,要实例化的组件
data?: Data; // 携带的数据,可通过 ActivatedRoute 获取
loadChildren?: LoadChildren; // 懒加载模块
matcher?: UrlMatcher; // 自定义匹配算
outlet?: string; // 当同级有多个 <router-outlet name="xxx"/> 时,指定从渲染出口
path?: string; // 匹配路径
pathMatch?: string; // 路径匹配策略,prefix(默认)|full
redirectTo?: string; // 匹配之后,要重定向的路径
resolve?: ResolveData; // 与路由关联的解析数据
runGuardsAndResolvers?: RunGuardsAndResolvers; // 定义 Guards 和 Resolvers 的运行时机
}
// 路由配置
const routes: Route[] = [
{
path: '/login',
canActivate: [LoginGuard],
// 懒加载只适用于“模块”,不适用于“组件”
loadChildren: () => import('./login/login.module').then(m => m.LoginModule)
},
// ...
{
path: '**',
component: NotFoundComponent
}
];
@NgModule({
imports: [
// forRoot 表示“这是一个根路由模块”,在一个应用中只会使用一次
RouterModule.forRoot(routes, {
/**
* enabled/enabledBlocking,引导程序被阻止,直到初始导航完成,一般 SSR 的时候才用得到
* enabledNonBlocking,引导程序不会被阻塞
* disabled,不执行初始导航
*/
initialNavigation: 'enabledNonBlocking',
anchorScrolling: 'enabled', // 当 URL 中有锚点时,是否滚动到锚点('disabled'|'enabled'|'top')
enableTracing: false, // 为 true 时,Router 会将每次导航过程中触发的“路由生命周期”输出到控制台
onSameUrlNavigation: 'ignore', // 当导航到当前地址是的行为('reload'|'ignore')
paramsInheritanceStrategy: 'emptyOnly', // 如何将参数、数据从父路由合并到子路由
preloadingStrategy: 'NoPreloading', // 预加载策略
scrollPositionRestoration: 'enabled', // 发生“导航回退”是否重置滚动位置('disabled'|'enabled'|'top')
relativeLinkResolution: 'legacy', // 相对路径的导航策略
urlUpdateStrategy: 'deferred', // 浏览器地址栏的更新策略(默认在导航成功之后再更新)
useHash: false, // 环境不支持 history API 时,可使用哈希导航
errorHandler: error => error, // 自定义错误处理
scrollOffset: () => [0, 0] // 路由导航后的滚动偏移量
})
],
exports: [RouterModule]
})
export class AppRoutingModule {}
|
路由钩子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Injectable({
providedIn: 'root'
})
class LoginGuard implements CanActive, CanActiveChild {
canActive(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const auth = this.authService.currentAuth();
const user = this.authService.currentUser();
if (route.url[0].path === 'login') {
sessionStorage.removeItem('auth');
sessionStorage.removeItem('user');
} else if (!auth || !user) {
return false;
}
return true;
}
}
|
在组件中获取路由信息:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
@Component({
selector: 'app-general'
})
export class GeneralComponent implements OnInit {
isExclusive: boolean;
get currentQueryParams() {
// 可以通过 router 获取查询参数
const { url, parseUrl } = this.router;
const { queryParams } = parseUrl(url);
return queryParams;
}
constructor(private route: ActivatedRoute, private router: Router) {}
ngOnInit() {
// 在组件初始化的时候,订阅路由查询参数的变化
this.route.queryParams.subscribe(params => {
this.handleFetchData(params);
});
// 订阅路由事件
this.router.events.subscribe(e => {
if (e instanceof NavigationEnd) {
this.handleNavigationEnd();
}
});
}
handleFetchData(params) {
// ...
}
handleNavigationEnd() {
// 在导航结束时
const { exclusive } = this.currentQueryParams;
this.isExclusive = String(exclusive) === 'true';
}
handleSwitchUrl($event, { exclusive = null, path = null }) {
$event.stopPropagation();
let temp = [];
let queryParams = null;
const queryParamsHandling = '';
if (path) {
temp = [path];
}
if (exclusive) {
queryParams = { exclusive };
}
// 相对于当前路由进行导航并重置 URL 中的查询参数
this.router.navigate(temp, {
relativeTo: this.route,
queryParams,
queryParamsHandling
});
}
}
|
依赖注入
依赖是指系统中的各个组成部分之间的相互关系;具体到 Angular,是指某个 class 执行其功能“所需的”服务(@Injectable)或对象。
在传统模式中,如果组件 A 依赖服务 B,A 会在内部创建一个 B 的实例;在依赖注入模式中,依赖项 B 会通过 A 的 constructor 注入(默认行为是 new 调用),保存在 A 的私有属性上。
当 class 需要某个依赖时,该依赖就会作为参数添加到 class 的构造函数中。当 Angular 实例化 class 时,DI 会在“注入器树”中查找(从局部开始,向上冒泡,直到 root)provider;如果没有找到 provider,会报错。
如果要保证外部注入的服务是自己(当前 class)独有的(
沙箱式隔离
),只需在自己的元数据 providers 中列出 Service 即可(就能拥有自己独有的服务实例)。
如果“允许依赖缺失”或者需要“限制依赖查找范围”,可以使用修饰符更改默认的依赖查找机制:
- @Host 表示禁止在“请求该依赖的组件”以上查找依赖;
- @Self 表示只在该组件的注入器中查找提供者;
- @SkipSelf 表示跳过局部注入器,向上查找;
- @Optional 表示找不到依赖就返回 null。
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
34
35
36
37
38
39
40
41
42
43
44
45
|
@Component({
providers: [
{
provide: AaService,
// 为 DI 令牌(依赖的标识)关联一个“固定的值”
useValue: 123
},
{
provide: BbService, // DI 令牌
/**
* 为公共类/默认类换上一个“替代实现”
* 令牌和注入内容不一定“同名”
*/
useClass: CcService // DI 内容
},
{
provide: AliasService,
/**
* 为服务创建别名,提供了两种途径来访问同一服务对象
* 也可以用来“收窄/简化”BbService
*/
useExisting: BbService
},
{
provide: Example,
// 通过“工厂函数”来创建依赖实例
useFactory: user => {
if (user.token) {
return new SignUp();
}
return new SignIn();
},
// 用于指定“工厂函数”所使用的依赖
deps: [User]
},
DdService
]
})
class ExampleComponent {
constructor(
@Inject(AaService) private aa: any,
@SkipSelf() private bb: BbService,
private eg: Example
) {}
}
|
双向绑定
Angular 也有 Vue 中的 v-model 机制(modelValue/update:modelValue),语法为 [(value)];实现思路同样也是基于“组件传值”和“自定义事件”(语法糖)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Component({
template:
'<div><app-input-model [(example)]="exampleValue"></app-input-model><span>{{ exampleValue }}</span></div>'
})
class FormComponent {
exampleValue = null;
}
@Component({
template:
'<input type="text" [value]="example" (input)="handleInput($event)" />',
selector: 'app-input-model'
})
class InputModelComponent {
@Input() example;
// 事件名称必须和 @Input 属性相对应,为 xxxChange
@Output() exampleChange = new EventEmitter<any>();
handleInput(e) {
// 官方文档中,在这里直接对 example 进行了修改,我表示不理解
this.exampleChange.emit(e.target.value);
}
}
|
绑定语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<my-component
#component
[@animation]="animation"
[(banana)]="banana"
[options]="options"
(output)="onOutput($event)"
(click)="onClick()"
></my-component>
<!-- 如果你有强迫症的话,Ng 支持另一种模版语法 -->
<my-component
ref-component
bind-animate-animation="animation"
bindon-banana="banana"
bind-options="options"
on-output="onOutput($event)"
on-click="onClick()"
></my-component>
|
模版变量
模版变量,顾名思义,就是存在于组件模版中的变量;模版变量可以引用:元素、指令、组件等:
- 如果在组件上声明变量,该变量就会引用该组件实例;
- 如果在原生 HTML 标签上声明变量,该变量就会引用该元素;
- 如果你在 <ng-template/> 上声明变量,该变量就会引用一个 TemplateRef 实例;
- 如果该变量在右侧指定了一个名字(例如,#var=“ngModel”),该变量就会引用所在元素上具有这个 exportAs 名字的指令或组件。
1
2
3
4
5
6
7
8
|
<!-- # 声明了一个模版变量 phone -->
<input #phone type="text" placeholder="phone number" />
<!-- button 通过模版变量获取到了 input 的 value -->
<button (click)="callPhone(phone.value)">take a call</button>
<!-- 模版输入变量 -->
<ng-template #heroRef let-hero let-i="index" let-even="!isOdd">
<span [class]="{ 'even-row': even }">{{ i }}: {{ hero.name }}</span>
</ng-template>
|
模版变量可以在“包含此模板变量”的模板中的任何地方引用它;既然使用模版变量的前提是“包含此模板变量”,那么决定模版边界的“结构型指令(例如,ngIf、ngFor、<ng-template/>)”也决定了模版变量的作用域。
网络请求
要使用 HttpClient,需要在模块中导入 HttpClientModule;一般情况下,在 AppModule 导入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
providers: [
{ provide: NZ_I18N, useValue: zh_CN },
{ provide: HTTP_INTERCEPTORS, useClass: SpinInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
],
imports: [BrowserModule, HttpClientModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
|
发起 Http 请求:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import {
HttpClient,
HttpErrorResponse,
HttpHeaders,
HttpParams
} from '@angular/common/http';
import { catchError, retry, throwError } from 'rxjs/operators';
@Injectable({})
export class ApiService {
constructor(private http: HttpClient) {}
options = {
// 自定义请求头
headers: new HttpHeaders({
'Content-Type': 'multipart/form-data',
Authorization: 'au9mtPT3WwkDxfs'
}),
// 指定要返回的响应内容(联合类型:'body'|'events'|'response')
observe: 'body' as const,
// 查询参数,会被 encode 进 URL 中
params: new HttpParams({}),
// 是否回报请求进度
reportProgress: true,
// 指定返回数据的格式(联合类型:'arraybuffer'|'blob'|'json'|'text')
responseType: 'json' as const,
withCredentials: true
};
getConfig(params) {
return this.http
.get(`${environment.apiHost}/api/v1`, {
...this.options,
params
})
.pipe(
retry(3), // 如果请求失败,最多重复请求三次
catchError(this.handleError) // 错误处理函数
);
}
updateConfig(body) {
return this.http.post(
`${environment.apiHost}/api/v1`,
body /* 请求体 */,
options /* 请求选项 */
);
}
modifyConfig(body) {
return this.http.put(
`${environment.apiHost}/api/v1`,
body /* 请求体 */,
options /* 请求选项 */
);
}
deleteConfig(params) {
return this.http.delete(`${environment.apiHost}/api/v1`, {
...this.options,
params
});
}
handleError(error) {
if (error.status === 401) {
// ...
}
return throwError(() => error);
}
}
|
特殊标签
<ng-container/>,类似于 Vue 模版中的 <template/> 或者 React.Fragment;无实际意义,用来对模板片段按照展示逻辑进行分组:
1
2
3
4
5
6
7
8
9
10
|
<!-- ng-container 最常使用的场景就是和 *ngFor 搭配 -->
<nz-select>
<ng-container *ngFor="let item of branchList">
<nz-option
*ngIf="item.show"
[nzValue]="item.brabank_id"
[nzLabel]="item.brabank_name"
></nz-option>
</ng-container>
</nz-select>
|
<ng-template/>,用来定义(声明)模版片段;默认情况下,定义的模版片段不会直接渲染,需要指定渲染的位置;一般配合结构性指令使用:
1
2
3
4
5
6
7
|
<!-- 当 seconds 被重置时,会条件渲染 #actionResendSMS 片段 -->
<ng-container *ngIf="seconds !== 0; else actionResendSMS">
<span>接受短信大约需要 {{ seconds }} 秒</span>
</ng-container>
<ng-template #actionResendSMS>
<a (click)="handleSendSMS()">重新获取验证码</a>
</ng-template>
|
<ng-container/> 搭配 <ng-template/> 可以构造作用域插槽,实现逻辑与 UI 的分离,从而实现更好的逻辑复用:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
// 封装逻辑
@Component({
selector: 'app-user-list-slot',
template: `
<ng-container
*ngTemplateOutlet="userListSlot; context: temp"
></ng-container>
`
})
class UserListSlotComponent implements OnChange {
/**
* 通过 userListSlot 指向一段 HTML 片段
* 可以使用 @ContentChild(TemplateRef)
* 那样无需我们指定“投射内容”
* 标签包含的所有 HTML
* 都是“投射内容”
*/
@Input() userListSlot: TemplateRef<any>;
@Input() formatFn = v => v;
@Input() groupId = null;
@Output() updated = new EventEmitter();
temp = {
loading: false,
list: []
};
constructor(private apiService: ApiService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes?.groupId) {
this.fetchUserList();
}
}
fetchUserList() {
this.temp.loading = true;
this.apiService
.getUserList({ groupId: this.groupId })
.subscribe(({ success = false, data = [] }) => {
if (success) {
this.temp.list = data.map(this.formatFn);
}
this.temp.list = [];
this.temp.loading = false;
this.updated.emit({
...temp,
originalData: data
});
});
}
}
// 使用插槽
@Component({
template: `
<div>...</div>
<app-user-list-slot
[userListSlot]="userListTemplate"
[formatFn]="userListFormatter"
[groupId]="groupId"
(updated)="(handleUserListUpdated)"
>
<ng-template
#userListTemplate
let-userList="list"
let-loadingUserList="loading"
>
<nz-select [nzLoading]="loadingUserList">
<nz-option
[nzValue]="option.id"
[nzLabel]="option.name"
*ngFor="let option of userList"
></nz-option>
</nz-select>
</ng-template>
</app-user-list-slot>
<div>...</div>
`
})
class DormitoryConfigureComponent {
groupId = 21;
userListFormatter = ({ id, name }) => ({ id, name });
handleUserListUpdated({ originalData }) {
console.log(originalData);
}
}
|
<ng-content/>,使用在组件内部,可以使组件
“接受外部插入进来的内容”
,相当于 Vue 模版中的 <slot/>:
- Angular 中并没有“具名插槽”的概念,但是 <ng-content/> 有一个 select 属性(CSS 选择器,不支持变量),可以起到对外部插入的内容进行过滤的作用;
- 相比于 <router-outlet/>,<ng-content/> 展示的内容是由外部插入的,<router-outlet/> 展示的内容是由路由表指定的;
- 如果你的组件包含不带 select 属性的 <ng-content/> 元素,则该实例将接收所有与其他 <ng-content/> 元素都不匹配的投影组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!-- 例如,nz-button 的 template -->
<i nz-icon nzType="loading" *ngIf="nzLoading"></i>
<ng-content></ng-content>
<!-- 使用时 -->
<a nz-button>例子</a>
<!--
渲染的结果,“例子”会被插入到 ng-content
当作 Vue 中的“具名插槽”理解就好
-->
<a nz-button>
<i nz-icon nzType="loading" *ngIf="nzLoading"></i>
例子
</a>
|
特殊选择器
- ::ng-deep、/deep/、>>>,将样式应用到全局;所以请在 ::ng-deep 之前带上 :host,防止污染全局样式;
- :host、:host(condition),宿主元素选择器/当“宿主元素”满足 condition 时;
- :host-context(condition),当“宿主的祖先元素”满足 condition 时。
ContentChild 与 ViewChild
@ViewChild() 从当前组件模版中获取组件对象(Vue/React 中 ref 的功能):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Component({
selector: 'user-list',
template: '<nz-spin><form #form></form><user-detail></user-detail></nz-spin>'
})
export class UserListComponent implements OnInit, AfterViewInit {
@ViewChild(NzSpinComponent) nzSpinRef: NzSpinComponent;
@ViewChild('form') formRef: ElementRef;
ngOnInit() {
// 在 ngOnInit 钩子中是获取不到的
console.log(this.nzSpinRef); // undefined
console.log(this.formRef); // undefined
}
ngAfterViewInit() {
// 通过 ViewChild 从当前模版中获取组件实例
console.log(this.nzSpinRef);
console.log(this.formRef); // form 标签上的 #form 是必须的
}
}
|
@ContentChild() 从当前组件模版的 <ng-content/> 中获取组件对象:
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
|
@Component({
selector: 'user-list',
template:
'<user-detail><span #span>用户交易</span><user-trade></user-trade></user-detail>'
})
export class UserListComponent {}
@Component({
selector: 'user-detail',
template:
"<h2>用户详情</h2><div class='user-detail'><ng-content></ng-content></div>"
})
export class UserDetailComponent implements OnInit, AfterViewInit {
@ContentChild('span') spanRef: ElementRef;
@ContentChild(UserTradeComponent) userTradeRef: UserTradeComponent;
ngOnInit() {
// 在 ngOnInit 钩子中是获取不到的
console.log(this.spanRef); // undefined
console.log(this.userTradeRef); // undefined
}
ngAfterViewInit() {
// 从 ng-content 中获取组件/DOM 元素
console.log(this.spanRef); // span 标签上的 #span 是必须的
console.log(this.userTradeRef);
}
}
|
说明:
- ContentChild 和 ViewChild 指向的类型不同(一个在当前模版中查找,一个在当前模版的 <ng-content/> 中查找);
- ViewChild 对应的是 ngAfterViewInit、ngAfterViewChecked;
- ContentChild 对应的是 ngAfterContentInit、ngAfterContentChecked;
- AfterView 关心的是 ViewChildren,ViewChildren 的元素标签会出现在当前组件的模版中;
- AfterContent 关心的是 ContentChildren,ContentChildren 被投射进当前组件中。
ChangeDetectorRef
首先,每个组件都有自己的变更检测器,一个应用包含多个组件,组件之间的层级关系形成了“组件树”。
Angular 的默认检测机制是:当一个组件发生变更时,无论它在组件树的什么位置,都会触发树中的所有的变更检测器;Ng 会从“自顶向下”,扫描树所有节点。
ChangeDetectorRef 为组件实例提供了自定义变更检测功能,可以将组件从变更检测树中添加或删除;ChangeDetectorRef 提供了五个方法:
- markForCheck(),在 ChangeDetectionStrategy.OnPush 策略下,将视图标记为“需要变更检测”;
- detach(),将组件从变更检测树中删除,在 reattach 被调用之前,不会再进行变更检测;
- reattach(),将之前被 detach 的组件添加到变更检测树中;
- checkNoChanges(),对该视图及其子视图进行变更检测,如有变化,报异常(仅在开发环境有效);
- detectChanges(),对该视图及其子视图手动进行一次变更检测。
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
|
class DataListProvider {
get data() {
// 假设数据量很大,并且变化频繁
return [1, 2, 3, 4, 5];
}
}
@Component({
template: `
<li *ngFor="let item of dataProvider.data">{{ item }}</li>
`
})
class GiantList {
constructor(
private ref: ChangeDetectorRef,
public dataProvider: DataListProvider
) {
// 放弃 Ng 的变更检测策略
ref.detach();
// 间隔 5 秒手动进行变更检测,提高应用性能
setInterval(() => {
this.ref.detectChanges();
}, 5000);
}
}
|
在开发的过程中,可能会遇到**
NG100 错误
**,特别是在 ngAfterViewInit 钩子中执行“手动”渲染操作时;原因是,在开发模式下,每次"变更检测"后,都会执行一次"附加检查",如果在附加检查过程中,视图和"变更检测"后的视图不一致,会报 NG100 错误。
解决方法是,将同步渲染逻辑改为异步(宏任务、微任务均可),或者同步调用 detectChanges 重新进行变更检测。
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
|
@Component({
template: `
<ng-container #vcf></ng-container>
`
})
export class CodesComponent implements OnInit, AfterViewInit {
@ContentChild(TemplateRef) content: TemplateRef<any>;
@ViewChild('vcf', { read: ViewContainerRef }) vcf;
list: CodeItem[] = [];
constructor(private cdf: ChangeDetectorRef) {}
ngOnInit(): void {
// ...
}
ngAfterViewInit() {
this?.vcf?.createEmbeddedView(this.content, {
handleChecked: (v, o) => this.handleChecked(o),
$implicit: this.list
});
// 渲染完后,进行一次变更检测
this.cdf.detectChanges();
}
// ...
}
|
Rx(ReactiveX)JS
RxJS 是 ReactiveX 在 JS 中的实现,相似的还有 RxJava、RxSwift…;ReactiveX 是 FRP(函数响应式编程)的践行者。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
// 创建器,用来创建“流(数据序列)”,返回值是一个 Observable 对象
import { def, from, interval, of, range, timer } from 'rxjs';
import { fromPromise } from 'rxjs/internal/observable/fromPromise';
// filter|map 是操作符,用来处理“流”
import { filter, map } from 'rxjs/operators';
new Observable<any>((observer: Observer<any>) => {
setTimeout(() => {
// 任何时间,都可以通过 next 将值传入“流”中
observer.next(1000);
}, 1000);
});
/**
* of 可以将“单一值”创建为“流”
* “单一值”可以为任何数据类型
*/
of(1, 2, 3, 4, 5)
.pipe(
filter(item => item % 2 === 1),
map(item => item * 3)
)
/**
* Observable 必须被 subscribe 之后才会“产生”数据
* 如果没有 subscribe,之前的 pipe 就不会执行
* 所以,在 Angular 中,对同一接口多次 subscribe 会导致发起多次请求
* subscribe 表示“可观察对象”与“观察者”之间“建立了联系”
*/
.subscribe(item => console.log(item)); // 3 9 15
// from 可以将数组的每一项放入“流”中
from([1, 2, 3, 4, 5])
.pipe(
filter(item => item % 2 === 1),
map(item => item * 3)
)
.subscribe(item => console.log(item)); // 3 9 15
// range 可以从“范围”产生“流”
range(1, 5)
.pipe(
filter(item => item % 2 === 1),
map(item => item * 3)
)
.subscribe(item => console.log(item)); // 3 9 15
const p = new Promise((resolve, reject) => {
console.log('Promise Begin...');
setTimeout(() => resolve('Promise Resolved...'), 1000);
});
/**
* fromPromise 接受一个 Promise 实例
* 当这个 Promise 状态确定之后,将输出值放入“流”中
* 即使没有 subscribe,这个 Promise 也会执行
* 从 6.0 开始,可以使用 from 替代 fromPromise
*/
fromPromise(p).subscribe(item => console.log(item));
/**
* defer 用来“惰性地”创建“流”
* 他接受一个“产生流”的“工厂函数”
* 当调用 defer 时,“流”还不存在,“流”会被“按需”创建
* 当“流”被“需要”的时候,才会调用“工厂函数”,产生“流”
* 只要没有 subscribe,“工厂函数”就不会执行,Promise 也就不会执行
*/
defer(() => from(p)).subscribe(item => console.log(item));
/**
* timer(首次等待时间,重复间隔时间)用来创建“定时器流”
* 他创建的是一种“无尽流”(务必要在适当的时候取消订阅)
* 数据会不断重复的被放入“流”中
* 有点像 setInterval
*/
timer(3000, 1000).subscribe(item => console.log(item)); // 0 1 2 3...
// interval(重复间隔时间),是 timer 的语法糖
interval(1000).subscribe(item => console.log(item)); // 0 1 2 3...
|
不同于创建器(调用后返回 Observable 对象),Subject 是一个实现了 Observable 接口的“类”;需要先进行 new 调用,然后将数据“手动”放入“流”中(subject.next(value))。
不同于普通的 Observable 对象(单播),new Subject 返回的是一个“多播”,可以被多次 subscribe,可以将值传递给多个观察者;所以常常被用来存放应用状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { Subject } from 'rxjs';
const subject = new Subject<unknown>();
subject.subscribe((res: unknown) => {
console.log('one', res);
});
subject.subscribe((res: unknown) => {
console.log('two', res);
});
// 每个 subscribe 都收到新值
subject.next(1); // one 1, two 1
subject.next(2); // one 2, two 2
// 通过 subject 将 observable 转换为“多播”
const observable = from([1, 2, 3]);
observable.subscribe(subject);
// one 1, two: 1
// one 2, two: 2
// one 3, two: 3
|
当 subscribe 被调用时,会返回一个“订阅凭证”;“订阅凭证”包含了一个 unsubscribe 方法,用来取消“订阅”(调用即可)。
1
2
3
4
5
6
|
import {Observable} from "rxjs
let ob = new Observable(o => o.next(window.performance.now()));
// 每次调用 subscribe 后生成了新的值,next 被调用了两次(数据产生于每次订阅后)
ob.subscribe(value => console.log(value)); // 4491799.400000002
ob.subscribe(value => console.log(value)); // 4491799.699999999
|
“可观察对象”的分类:
- “冷”可观察对象,在 subscribe 之后,才将“值”添加到“流”中;有多少个订阅就会生成多少个订阅实例(单播,订阅接收到的值是相同的);
- “热”可观察对象,在 subscribe 之前,已经产生了“值”;多个订阅共享同一个实例(多播,订阅接收到的值取决于什么时间开始订阅的)。
特殊的“可观察对象”:
- AsyncSubject,多播;只有当 complete 时,才会将“最后一个值”发送给订阅者;
- BehaviorSubject,多播;保存了发送出去的“最后一个值”,“新的”订阅者会接收到这个值;
- ReplaySubject,多播;保存了发送出去的“所有值”,“新的”订阅者会接收到所有“旧”值。
AOT 与 JIT
组件和模版需要经过编译,才能在浏览器中运行;Angular 提供了两种编译方式:
- Ahead-Of-Time 预编译(在打包/构建时编译,Angular 9 之后):
- 和别的模版一样,*.vue 和 *.jsx 也需要进行编译才能在浏览器中运行;
- 可以尽早地检测到代码中的错误,用户体验更有保障;
- 省去了在运行时编译的开销,应用响应更快;
- 减少了受 XSS 攻击的风险;
- 可以减小应用的体积。
- Just-In-Time 即时编译(在运行时编译,Angular 9 之前):
- 代码修改之后不需要重新编译,编译会在代码执行之前进行(按需编译);
- 编译工作大多在浏览器内部进行,编译时间更短;
- 在项目中,较少被用到的组件更适合即时编译。
AOT 预编译分为三个阶段:
- 代码分析:收集元数据并检验元数据中的错误:
- 被收集的元数据存在于类装饰器/构造函数中;
- AOT 收集器会根据 TS 编译器生成的 *.d.ts 输出 *.metadata.json;
- *.metadata.json 包括了全部装饰器的元数据,结构就像 AST 一样;
- AOT 编译器不支持“箭头函数”,所以不要在元数据中使用“() => {}”。
- 代码生成:解释元数据(*.metadata.json),并对含有 useClass、useValue、useFactory 和 data 的对象字面量进行特殊处理(元数据重写);
- 模版类型检查:TS 编译器会验证模版中绑定的表达式。
JIT 编译器可以替代 AOT 编译器,在“运行时”将模板编译(需要将 compiler 打包)为浏览器中的可执行模板代码。
安全
为了防范 XSS,Angular 认为所有值都是不可信任的;任何值在插入 DOM 之前,都会被进行“无害化”处理(前提是使用 Angular 模版,如果在代码中直接使用 DOM API,将没有“无害化”处理)。
Angular 定义了四个安全上下文:
- HTML:被解释为 HTML 的值,例如被绑定到 innerHTML 的值;
- CSS:作为 CSS 被绑定到 style 属性的值;
- 网址:作为 URL 使用的值,例如被绑定到 a 标签 href 属性的值;
- 资源:作为代码需要被下载并运行的值,例如被绑定到 script 标签 src 属性的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Component({
template: `
<!-- {{ 插值中的内容总会被“无害化”}} -->
<p class="e2e-inner-html-interpolated">{{ htmlSnippet }}</p>
<!--
绑定到 innerHTML 属性上的值,默认被认为是“不安全”的
会被自动进行“无害化”
<script> 会被移除
-->
<p class="e2e-inner-html-bound" [innerHTML]="htmlSnippet"></p>
`
})
export class InnerHtmlComponent {
htmlSnippet = '<script>alert("...")</script><b>Syntax</b>';
}
|
如果某个值被确认是可信的,可以通过
DomSanitizer
将这个值标记为安全值,安全的值将不会被进行“无害化”。
DomSanitizer 提供了六种方法:
- sanitize(context, value):根据给定的安全上下文 context,对 value 进行“无害化”;
- bypassSecurityTrustHtml(value):信任给定的值 value 是一个安全的 HTML;
- bypassSecurityTrustStyle(value):信任给定的值 value 是一个安全的样式;
- bypassSecurityTrustScript(value):信任给定的值 value 是一个安全的 JS;
- bypassSecurityTrustUrl(value):信任给定的值 value 是一个安全的 URL;
- bypassSecurityTrustResourceUrl(value):信任给定的值 value 是一个安全的资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Component({
template: `
<h4>自动无害化处理</h4>
<p><a [href]="dangerousUrl">Click Me</a></p>
<h4>不会进行无害化处理</h4>
<p><a [href]="trustedUrl">Click Me</a></p>
`
})
export class BypassSecurityTrustComponent {
constructor(private sanitizer: DomSanitizer) {}
dangerousUrl = 'javascript:alert("Hello")';
trustUrl = this.sanitizer.bypassSecurityTrustUrl(this.dangerousUrl);
}
|
参考