Angular 中的装饰器

Angular 中的装饰器

Angular 中的装饰器可以简单的总结为以下几句

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为 targetnamedescriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象

它分为一下四类

  • 类装饰器 (Class decorators
  • 属性装饰器 (Property decorators
  • 方法装饰器 (Method decorators
  • 参数装饰器 (Parameter decorators

TypeScript 中的装饰器

先来了解一下 TypeScript 当中的装饰器,它们有以下这些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 类装饰器,用来装饰类的,它接收一个参数
// target: TFunction,被装饰的类
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void

// 属性装饰器,用来装饰类的属性,它接收两个参数
// target: Object,被装饰的类
// propertyKey: string | symbol,被装饰类的属性名
declare type PropertyDecorator = (target:Object, propertyKey: string | symbol ) => void

// 方法装饰器,用来装饰类的属性,它接收三个参数
// target: Object,被装饰的类
// propertyKey: string | symbol,方法名
// descriptor: TypePropertyDescript,属性描述符
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void

// 参数装饰器,用来装饰函数参数,它接收三个参数
// target: Object,被装饰的类
// propertyKey: string | symbol,方法名
// parameterIndex: number,方法中参数的索引值
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void

Angular 内置装饰器

Angular 当中已经为我们内置了一些装饰器,如下

  • 类装饰器:@Component@NgModule@Pipe@Injectable
  • 属性装饰器:@Input@Output@ContentChild@ContentChildren@ViewChild@ViewChildren
  • 方法装饰器:@HostListener@HostBinding
  • 参数装饰器:@Inject@Optional@Self@SkipSelf@Host

有些是默认生成组件的时候就自带了的,有的使用频率也较低,所以我们在这里主要介绍 @Input@Output@ViewChild@ViewChildren@HostListener@HostBinding 六种

Input

Input 是属性装饰器,用来定义组件内的输入属性,一般用来实现父组件向子组件传递数据

@Input()

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
// counter.component.ts
import { Component, Input } from '@angular/core'
@Component({
selector: 'exe-counter',
template: `
<p>当前值: {{ count }}</p>
<button (click)="increment()"> + </button>
<button (click)="decrement()"> - </button>
`
})
export class CounterComponent {
@Input() count: number = 0

increment() {
this.count++
}

decrement() {
this.count--
}
}


// app.component.ts
import { Component } from '@angular/core'

@Component({
selector: 'exe-app',
template: `
<exe-counter [count]="initialCount"></exe-counter>
`
})

export class AppComponent {
initialCount: number = 5
}

@Input(bindingPropertyName)

Input 装饰器支持一个可选的参数,用来指定组件绑定属性的名称,如果没有指定,则默认使用 @Input 装饰器装饰的属性名,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
// counter.component.ts
export class CounterComponent {
@Input('value') count: number = 0
}

// app.component.ts
@Component({
selector: 'exe-app',
// 绑定的时候如果写成 [value],那么在 @Input() 接收的时候指定为 value 即可
template: `
<exe-counter [value]="initialCount"></exe-counter>
`
})

inputs

另外还可以使用 inputs 属性将绑定的输入属性名称直接写到 @Component({}) 的元数据当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// counter.component.ts
export class CounterComponent {
@Input('value') count: number = 0
}

// app.component.ts
@Component({
selector: 'exe-app',
template: `
<exe-counter [value]="initialCount"></exe-counter>
`,
// 如果模版当中指定的为 [count]="initialCount"
// 可以直接写为 inputs: ['count']
inputs: ['count: value']
})

export class CounterComponent {
count: number = 0
}

不过需要注意的是,不能同时使用 @Input 装饰器,或在 @Directive@Component inputs 字段中定义同一个输入属性

1
2
3
4
5
6
7
8
9
// 错误的使用方式
@Component({
selector: 'exe-counter',
inputs:['count: value']
})

export class CounterComponent {
@Input('value') count: number = 0
}

@Input 和 inputs 两者的区别

它们都是用来定义输入属性,而不同的地方在于

  • inputs 定义在指令的 metadata 信息中,开发者对指令的输入属性一目了然
  • 此外对于未选用 TypeScript 作为开发语言的开发者,也只能在 metadata 中定义指令的输入属性

@Input 属于属性装饰器,通过它可以一起定义属性的访问描述符(publicprivateprotected

1
@Input() public attr: string

@Output 与其类似

1
@Output('countChange') change: EventEmitter<number> = new EventEmitter<number>()

setter & getter

settergetter 是用来约束属性的设置和获取,它们提供了一些属性读写的封装,可以让代码更便捷,更具可扩展性,通过 settergetter 方式,我们对类中的私有属性进行了封装,能避免外界操作影响到该私有属性

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 { Component, Input } from '@angular/core'

@Component({
selector: 'exe-counter',
template: `
<p>当前值: {{ count }} </p>
<button (click)="increment()"> + </button>
<button (click)="decrement()"> - </button>
`
})

export class CounterComponent {
_count: number = 0
biggerThanTen: boolean = false

@Input()
set count (num: number) {
this.biggerThanTen = num > 10
this._count = num
}

get count(): number {
return this._count
}

increment() {
this.count++
}

decrement() {
this.count--
}
}

Output

Output 是属性装饰器,用来定义组件内的输出属性,主要用来实现子组件将信息通过事件的形式通知到父级组件

EventEmitter

Output 属性装饰器一般是和 EventEmitter 一起相互配合来使用的,先看 EventEmitter

1
2
3
4
5
let numberEmitter: EventEmitter<number> = new EventEmitter<number>()

numberEmitter.subscribe((v: number) => console.log(v))

numberEmitter.emit(10)

具体的应用流程为

  • 子指令创建一个 EventEmitter 实例,并将其作为输出属性导出
  • 子指令调用已创建的 EventEmitter 实例中的 emit(payload) 方法来触发一个事件
  • 而父指令通过事件绑定(eventName)的方式监听该事件,并通过 $event 对象来获取 payload 对象
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
// counter.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core'

@Component({
selector: 'exe-counter',
template: `
<p>当前值: {{ count }}</p>
<button (click)="increment()"> + </button>
<button (click)="decrement()"> - </button>
`
})
export class CounterComponent {

@Input() count: number = 0
@Output() change: EventEmitter<number> = new EventEmitter<number>()

increment() {
this.count++
this.change.emit(this.count)
}

decrement() {
this.count--
this.change.emit(this.count)
}
}


// app.component.ts
import { Component } from '@angular/core'

@Component({
selector: 'exe-app',
template: `
<exe-counter [count]="initialCount"
(change)="countChange($event)"></exe-counter>
`
})
export class AppComponent {

initialCount: number = 5

countChange(ev: number) {
console.log(ev)
}
}

@Output(bindingPropertyName)

@Input 一样,@Output 装饰器支持一个可选的参数,用来指定组件绑定属性的名称,如果没有指定,则默认使用 @Output 装饰器,装饰的属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
// counter.component.ts
export class CounterComponent {
@Output('counterChange') change: EventEmitter<number> = new EventEmitter<number>()
}


// app.component.ts
@Component({
template: `
<exe-counter [count]="initialCount"
(counterChange)="countChange($event)"></exe-counter>
`
})

outputs

inputs 一样,也支持在组建内部添加 outputs 属性,但是和上面一样不太推荐这种写法,所以在这里也不过多的进行介绍了

ngOnChanges

当数据绑定输入属性的值发生变化的时候,Angular 将会主动调用 ngOnChanges 方法,它会获得一个 SimpleChanges 对象,包含绑定属性的新值和旧值,它主要用于监测组件输入属性的变化

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
import { Component, Input, SimpleChanges, OnChanges } from '@angular/core'

@Component({
selector: 'exe-counter',
template: `
<p>当前值: {{ count }}</p>
<button (click)="increment()"> + </button>
<button (click)="decrement()"> - </button>
`
})

export class CounterComponent implements OnChanges{
@Input() count: number = 0

ngOnChanges(changes: SimpleChanges) {
console.dir(changes['count'])
}

increment() {
this.count++
}

decrement() {
this.count--
}
}

可以在控制台当中看到 SimpleChanges 对象的一些值(包括新值和旧值)

需要注意的是,当手动改变输入属性的值,是不会触发 ngOnChanges 钩子的

@ViewChild()

通过 @ViewChild() 装饰器可以获得子组件的引用,从而可以在父组件当中来直接调用子组件的方法

1
2
// 父组件
<app-header #child1></app-header>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class AppComponent implements OnInit{

@ViewChild('child1')
child1: HeaderComponent

ngOnInit() {
this.child1.run('...')
}
}

// 子组件
export class HeaderComponent implements OnInit {
constructor() { }
ngOnInit() { }

run(name) {
console.log(name)
}
}

@ViewChild 使用类型查询

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
// child.component.ts
import { Component, OnInit } from '@angular/core'

@Component({
selector: 'exe-child',
template: `
<p>Child Component</p>
`
})
export class ChildComponent {
name: string = 'child-component'
}


// app.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core'
import { ChildComponent } from './child.component'

@Component({
selector: 'my-app',
template: `
<h4>Welcome to Angular World</h4>
<exe-child></exe-child>
`,
})
export class AppComponent {

// 通过 @ViewChild() 来获取子组件
@ViewChild(ChildComponent)
childCmp: ChildComponent

ngAfterViewInit() {
console.dir(this.childCmp)
}
}

ViewChildren

ViewChildren 用来从模版视图中获取匹配的多个元素,返回的结果是一个 QueryList 集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component, ViewChildren, QueryList, AfterViewInit } from '@angular/core'
import { ChildComponent } from './child.component'

@Component({
selector: 'my-app',
template: `
<h4>Welcome to Angular World</h4>
<exe-child></exe-child>
<exe-child></exe-child>
<exe-child></exe-child>
`,
})
export class AppComponent {

@ViewChildren(ChildComponent)
childCmps: QueryList<ChildComponent>

ngAfterViewInit() {
console.dir(this.childCmps)
}
}

运行之后可以在控制台当中看到输出多个 ChildComponent

小结

  • ViewChild 装饰器用于获取模板视图中的元素,它支持 Type 类型或字符类型的选择器,同时支持设置 read 查询条件,以获取不同类型的实例
  • ViewChildren 装饰器是用来从模板视图中获取匹配的多个元素,返回的结果是一个 QueryList 集合

HostListener & HostBinding

在介绍 HostListenerHostBinding 属性装饰器之前,我们可以先来了解一下 Host Element(宿主元素),宿主元素的概念同时适用于指令和组件,对于指令来说,应用指令的元素就是『宿主元素』,而如果在自定义组件中使用的话,那么自定义组件就是宿主元素

HostListener

HostListener 是属性装饰器,用来为宿主元素添加事件监听,HostListenerDecorator 装饰器定义如下

1
2
3
4
export interface HostListenerDecorator {
(eventName: string, args?: string[]): any
new (eventName: string, args?: string[]): any
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Directive, HostListener } from '@angular/core'

@Directive({
selector: 'onClicks'
})

export class onClicks {

@HostListener('click')
onClick() {
// ...
}

}

此外还可以监听宿主元素外,其他对象产生的事件,比如 windowdocument 对象,一个点击目标区域会添加背景颜色,点击其他区域取消掉高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export class SetBackgroundDirective {

constructor(
private el: ElementRef,
private re: Renderer2
) {}

@HostListener('document:click', ['$event'])
onClick(btn: Event) {
if (this.el.nativeElement.contains(event.target)) {
this.highlight('yellow')
} else {
this.highlight(null)
}
}

highlight(color: string) {
this.re.setStyle(this.el.nativeElement, 'backgroundColor', color)
}
}

Host Event Listener

还可以使用 host 参数来进行绑定(不太建议使用这种方式,推荐使用装饰器风格)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Directive } from '@angular/core'

@Directive({
selector: 'button[counting]',
host: {
'(click)': 'onClick($event.target)'
}
})
export class CountClicks {
numberOfClicks = 0

onClick(btn: HTMLElement) {
console.log('button', btn, 'number of clicks:', this.numberOfClicks++)
}
}

HostBinding

HostBinding 是属性装饰器,用来动态设置宿主元素的属性值,定义如下

1
2
3
4
export interface HostBindingDecorator {
(hostPropertyName?: string): any
new (hostPropertyName?: string): any
}

应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Directive({
selector: '[exeButtonPress]'
})

export class ExeButtonPress {
@HostBinding('attr.data') data = 'button'
@HostBinding('class.active') isActive: boolean

@HostListener('mouseenter')
enter() {
this.isActive = true
}

@HostListener('mouseleave')
leave() {
this.isActive = false
}
}

和上面一样,我们也可以在指令的元数据当中来进行绑定(同样的不建议这样使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Directive({
selector: '[exeButtonPress]',
host: {
'data': 'button',
'[class.active]': 'isActive'
}
})

export class ExeButtonPress {
isActive: boolean

@HostListener('mouseenter')
enter() {
this.isActive = true
}

@HostListener('mouseleave')
leave() {
this.isActive = false
}
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×