装饰器在Angular中是个很常见的概念,@Component@Directive@Pipe@NgModule@Injectable@Input@Inject等等.本文着重讲解各种装饰器的作用以及在Angular中的实际使用


装饰工厂

装饰器工厂只是一个函数,它返回将由装饰器在运行时调用的表达式

function color(value: string) {
// this is the decorator factory, it sets up
// the returned decorator function
return function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}


类装饰

类型注释:

type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
  • @参数:

    1.target:类的构造函数

  • @Returns

    如果类装饰器返回一个值,它将替换类声明。
    因此,它适用于扩展具有某些属性或方法的现有类。

function toString() {
return function <T extends { new (...args: any[]): any }>(constructor: T) {
return class extends constructor {
toString() {
return JSON.stringify(this);
}
};
};
}

@toString()
class C {
public foo = "foo";
public num = 24;
}

console.log(new C().toString())
// -> {"foo":"foo","num":24}


方法装饰

类型注释:

type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

  • @参数:

    1.target:类的构造函数或实例成员的类的原型

    2.propertyKey:属性名称

    3.descriptor:成员的属性描述符

  • @Returns

    如果返回一个值,它将作为成员的描述符

function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;

descriptor.value = function (...args) {
console.log('params: ', ...args);
const result = original.call(this, ...args);
console.log('result: ', result);
return result;
}
}

class C {
@logger
add(x: number, y:number ) {
return x + y;
}
}

const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3


属性装饰

类型注释:

type PropertyDecorator = 
(target: Object, propertyKey: string | symbol) => void;
  • @参数:

    1.target:类的构造函数或实例成员的类的原型

    2.propertyKey:属性名称

  • @Returns

    如果返回一个值,它将作为属性的描述符

class Greeter {
@format("Hello, %s")
greeting: string;

constructor(message: string) {
this.greeting = message;
}
}

function format(value:string){
return function(target:any,propertyKey:string){
return {
configurable: true,
writable: true,
value:value
};
}
}


访问器装饰

访问器装饰器通常与方法装饰器相同,唯一的区别是描述符中的键:

方法装饰器中的描述符具有键:

  • value
  • writable
  • enumerable
  • configurable

访问器装饰器中的描述符具有键:

  • get
  • set
  • enumerable
  • configurable


参数装饰

类型注释:

type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
  • @参数:

    1.target:类的构造函数或实例成员的类的原型

    2.propertyKey:属性名称(方法名,不是参数名)

    3.parameterIndex:函数参数列表中参数的序号索引

  • @Returns

    返回值将被忽略。


实例

有如下的button组件,在传入danger输入属性时,根据不同的值,传入不同的值以此来改变按钮样式,

..
@Component({
selector: 'button[my-button]',
exportAs: 'myButton',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
template: ` <ng-content></ng-content> `,
host: {
class: 'btn',
'[class.btn-dangerous]': `danger`, //danger参数决定是否加载样式
},
})
export class ButtonComponent {
@Input() danger : boolean= false;
}

实际使用时,组件的使用:<button my-button [danger]="true">btn</button>,danger的入参过于繁琐,通过给danger添加装饰器来达到将不符合的类型值转化为布尔值;已达到<button my-button danger>btn</button>的效果

定义参数装饰器,引入@angular/cdkcoerceBooleanProperty方法将值转换为布尔;

function InputBoolean(){
return function ( target: any,propertyKey: string){
const privatePropName = `$$__private__${propName}`;

//创建新的属性并设置属性描述
Object.defineProperty(target, privatePropName, {
configurable: true,
writable: true,
});

/**
* 返回新的属性描述
* 来达到如下效果
* // @Input()
* // get visible() { return this.__visible; }
* // set visible(value) { this.__visible = value; }
* // __visible = false;
*/
return {
get(): string {
this[privatePropName];
},
set(value: T): void {
this[privatePropName] = coerceBooleanProperty(value);
},
};
}
}

之后使用定义好的装饰器,此时使用组件并设置danger参数时,angular会有Type 'string' is not assignable to type 'boolean'的报错

...
export class ButtonComponent {
@Input() @InputBoolean() danger : boolean= false;
}

//danger会提示报错 
<button my-button danger>btn</button>

此时通过添加ngAcceptInputType_前缀来放宽入参的类型


export type BooleanInput = boolean | string | undefined | null;

...
export class ButtonComponent {
static ngAcceptInputType_danger: BooleanInput;

@Input() @InputBoolean() danger : boolean= false;
}

完整代码

//button.component.js
...
@Component({
selector: 'button[my-button]',
exportAs: 'myButton',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
template: ` <ng-content></ng-content> `,
host: {
class: 'ant-btn',
'[class.ant-btn-dangerous]': `danger`,
},
})
export class ButtonComponent {
static ngAcceptInputType_danger: BooleanInput;

@Input() @InputBoolean() danger = false;

constructor() {}
}

//util/convert.ts

...
import {
coerceBooleanProperty,
coerceCssPixelValue,
_isNumberValue,
} from '@angular/cdk/coercion';

export function toBoolean(value: boolean | string): boolean {
return coerceBooleanProperty(value);
}

function propDecoratorFactory<T, D>(
name: string,
fallback: (v: T) => D
): (target: any, propName: string) => void {
function propDecorator(
target: any,
propertyKey: string,
originalDescriptor?: TypedPropertyDescriptor<any>
): any {
const privatePropName = `$$__private__${propertyKey}`;

if (Object.prototype.hasOwnProperty.call(target, privatePropName)) {
console.warn(
`The prop "${privatePropName}" is already exist, it will be overrided by ${name} decorator.`
);
}

Object.defineProperty(target, privatePropName, {
configurable: true,
writable: true,
});

return {
get(): string {
return originalDescriptor && originalDescriptor.get
? originalDescriptor.get.bind(this)()
: this[privatePropName];
},
set(value: T): void {
if (originalDescriptor && originalDescriptor.set) {
originalDescriptor.set.bind(this)(fallback(value));
}

this[privatePropName] = fallback(value);
},
};
}

return propDecorator;
}

export function InputBoolean(): any {
return propDecoratorFactory('InputBoolean', toBoolean);
}
//convert-input.ts
export type BooleanInput = boolean | string | undefined | null;