第四篇:TypeScript装饰器

第三篇的文章中,我们实现了简单的IoC容器,代码如下:

 1 import 'reflect-metadata';
 2 
 3 type Tag = string;
 4 type Constructor<T = any> = new (...args: any[]) => T;
 5 type BindValue = string | Function | Constructor<any>;
 6 
 7 export function Injectable(constructor: Object) {
 8 
 9 }
10 
11 export class Container {
12     private bindTags: any = {};
13     public bind(tag: Tag, value: BindValue) {
14         this.bindTags[tag] = value;
15     }
16     public get<T>(tag: Tag): T {
17         const target = this.getTagValue(tag) as Constructor;
18         const providers: BindValue[] = [];
19         for(let i = 0; i < target.length; i++) {
20             // 获取参数的名称
21             const providerKey = Reflect.getMetadata(`design:paramtypes`, target)[i].name;
22             // 把参数的名称作为Tag去取得对应的类
23             const provider = this.getTagValue(providerKey);
24             providers.push(provider);
25         }
26         return new target(...providers.map(p => new (p as Constructor)()))
27     }
28     private getTagValue(tag: Tag): BindValue {
29         const target = this.bindTags[tag];
30         if (!target) {
31             throw new Error("Can not find the provider");
32         }
33         return target;
34     }
35 }
container.ts
 1 import { Injectable } from "./container";
 2 
 3 @Injectable
 4 export class Hand {
 5     public hit() {
 6         console.log('Cust!');
 7         return 'Cut!';
 8     }
 9 }
10 @Injectable
11 export class Mouth {
12     public bite() {
13         console.log('Hit!');
14         return 'Hit!';
15     }
16 }
17 @Injectable
18 export class Human {
19     private hand: Hand;
20     private mouth: Mouth;
21     constructor(
22         hand: Hand,
23         mouth: Mouth
24     ) {
25         this.hand = hand;
26         this.mouth = mouth;
27     }
28     public fight() {
29         return this.hand.hit();
30     }
31     public sneak() {
32         return this.mouth.bite();
33     }
34 }
test-class.ts

下面是怎么使用Ioc Container:

 1 import 'reflect-metadata';
 2 
 3 import { Container } from "./container";
 4 import { Hand, Human, Mouth } from "./test-class";
 5 
 6 const container = new Container();
 7 container.bind('Hand', Hand);
 8 container.bind('Mouth', Mouth);
 9 container.bind('Human', Human);
10 
11 const human = container.get<Human>('Human');
12 
13 human.fight();
14 human.sneak();

 可以看到,bind方法使用的方式有点奇怪,我们使用装饰器来改造一下。

 

装饰器是一种特殊类型的声明,它能够被附件到类声明,方法,访问符,属性,或者参数上。

装饰器使用@expression这种形式,其中,expression必须是函数。

比如

function sealed(target) {
    // do something with "target" ...
}

 

如果想要给expression传递一些我们自定义的参数,则需要使用装饰器工厂函数,其实这个装饰器工厂函数就是返回装饰器函数的函数,比如:

1 function color(value: string) { // 这是一个装饰器工厂
2     return function (target) { //  这是装饰器
3         // do something with "target" and "value"...
4     }
5 }

 

 如果有多个装饰器应用在同一个声明上,比如

1 // 书写在同一行
2 @f @g x
3 // 书写在不同行
4 @f
5 @g
6 x

 

 在Typescript中,当多个装饰器应用在一个声明上时,会进行如下步骤的操作。

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作装饰器函数,由下至上依次被调用。
 1 function f() {
 2     console.log("f(): evaluated");
 3     return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
 4         console.log("f(): called");
 5     }
 6 }
 7 
 8 function g() {
 9     console.log("g(): evaluated");
10     return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
11         console.log("g(): called");
12     }
13 }
14 
15 class C {
16     @f()
17     @g()
18     method() {}
19 }

 

执行结果 

1 f(): evaluated
2 g(): evaluated
3 g(): called
4 f(): called

 

 即求值由上到下,调用由下到上。

那么,如果一个类中有多个装饰器装饰不同的声明,那么它的调用规则是怎么样子的?

 

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

总的来说,从小到大(参数 ==》 方法 ==》 访问符 ==》 属性 ==》 构造函数 ==》 类),和多个装饰器应用到一个声明上的调用规则是一样的。

一.类装饰器

类装饰器应用在类声明前面,可以用来监控,修改,替换类定义。

类装饰器函数只有一个参数,就是这个类的构造函数

需要特别这样的是,这里的构造函数不是constructor函数,而且构造整个类的函数,

比如下面这个类

1 class Greeter {
2     property = "property";
3     hello: string;
4     constructor(m: string) {
5         this.hello = m;
6     }
7 }

 

传递给装饰器函数的不是

1     constructor(m: string) {
2         this.hello = m;
3     }

 

而是:

1     function Greeter(m) {
2         this.property = "property";
3         this.hello = m;
4     }

 

可以理解为就是把整个类传递给了装饰器函数。

 

类装饰器可以不返回值,这时对类定义进行修改。

 1 function sealed(constructor: Function) {
 2     Object.seal(constructor);
 3     Object.seal(constructor.prototype);
 4 }
 5 @sealed
 6 class Greeter {
 7     greeting: string;
 8     constructor(message: string) {
 9         this.greeting = message;
10     }
11     greet() {
12         return "Hello, " + this.greeting;
13     }
14 }

 

如果类装饰器返回一个值,那么这个值将会被当作新的构造函数。

需要关注的是,返回的构造函数需要自己处理原型链。

 

 1 function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
 2     return class extends constructor {
 3         newProperty = "new property";
 4         hello = "override";
 5     }
 6 }
 7 
 8 @classDecorator
 9 class Greeter {
10     property = "property";
11     hello: string;
12     constructor(m: string) {
13         this.hello = m;
14     }
15 }
16 
17 console.log(new Greeter("world"));

 

 结果是:

1 class_1 {
2   property: 'property',
3   hello: 'override',
4   newProperty: 'new property'
5 }

 

 可以看到,虽然在装饰器中,返回的构造函数extends了用来的构造函数,但是,hello属性的值还是被覆盖掉了。我们来看一下typescript编译成JavaScript的代码:

 1 function classDecorator(constructor) {
 2     console.log(constructor);
 3     return /** @class */ (function (_super) {
 4         __extends(class_1, _super);
 5         function class_1() {
        // 这里,继承了原来的类
6 var _this = _super !== null && _super.apply(this, arguments) || this;
         // 这里,新加了一些属性
7 _this.newProperty = "new property";
        // 这里,修改了一些属性
8 _this.hello = "override";
        // 因为继承了原来的类,使用类中的property属性还是在的
9 return _this; 10 } 11 return class_1; 12 }(constructor)); 13 } 14 var Greeter = /** @class */ (function () {
     // 注意这里,传递给装饰器还是的就是整个类,不是普通意义上的构造函数。
15 function Greeter(m) { 16 this.property = "property"; 17 this.hello = m; 18 } 19 Greeter = __decorate([ 20 classDecorator, 21 __metadata("design:paramtypes", [String]) 22 ], Greeter); 23 return Greeter; 24 }());

 

二.方法装饰器

方法装饰器声明在一个方法的声明之前,它会被应用到方法的属性描述符上。

这里简单记录一下属性描述符:

  • value:设置属性值,默认值为 undefined。
  • writable:设置属性值是否可写,默认值为 true。
  • enumerable:设置属性是否可枚举,即是否允许使用 for/in 语句或 Object.keys() 函数遍历访问,默认为 true。
  • configurable:设置是否可设置属性特性,默认为 true。如果为 false,将无法删除该属性,不能够修改属性值,也不能修改属性的属性描述符。
  • get:取值函数,默认为 undefined。
  • set:存值函数,默认为 undefined。

方法装饰器函数有3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
  2. 成员的名字
  3. 成员的属性描述符

如果方法装饰器返回一个值,那么这个值会被当作成员的属性描述符。这给成员的定义提供了极大的灵活性。

下面是替换属性描述符的例子:

 1 class Greeter {
 2     greeting: string;
 3     constructor(message: string) {
 4         this.greeting = message;
 5     }
 6 
 7     @OtherGreet(false)
 8     greet() {
 9         return "Hello, " + this.greeting;
10     }
11 }
12 function OtherGreet(value: boolean) {
13     return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
14         console.log(target);
15         return {
16             ...descriptor,
17             value: () => {
18                 return 'No Hello,' + target.greeting;
19             }
20         }
21     };
22 }
23 const g = new Greeter('wt');
24 console.log(g.greet())

 

返回的值:

Greeter { greet: [Function] }
No Hello,undefined

 

这个例子有需要可以思考的地方,第一点,为什么target是Greeter { greet: [Function] },而没有包含greeting. 这是因为,当方法构造器作用在实例成员上时,target是原型对象,而属性是直接作用在对象上的,只有方法是在原型对象上。

我们可以看一下TypeScript的class编译成js的结果:

var Greeter = /** @class */ (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    // @OtherGreet(false)
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
}());

 

可以看到,只有greet方法在原型上。那么,我们怎么才能在方法构造器中访问到类中的其他属性成员呢?   我也不知道。。。。

这也是为什么出现undefined的原因。

三.访问器装饰器

访问器装饰器作用在一个访问器的声明之前,应用于访问器的属性描述符

访问器装饰器有3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象
  2. 成员的名字
  3. 成员的属性描述符

如果访问器装饰器返回一个值,它会被当作新的属性描述符来覆盖原来的属性描述符。

 1 class Point {
 2     private _x: number;
 3     private _y: number;
 4     constructor(x: number, y: number) {
 5         this._x = x;
 6         this._y = y;
 7     }
 8 
 9     @configurable(false)
10     get x() { return this._x; }
11 
12     @configurable(false)
13     get y() { return this._y; }
14 }
15 function configurable(value: boolean) {
16     return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
17         descriptor.configurable = value;
18     };
19 }

 

四.属性装饰器

属性装饰器声明在一个属性声明之前。

属性装饰器有2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
  2. 成员的名称
注意:属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 
因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。
因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

 

所以,使用属性装饰器时,一般配合元数据一起使用。

 1 class Greeter {
 2     @format("Hello, %s")
 3     greeting: string;
 4 
 5     constructor(message: string) {
 6         this.greeting = message;
 7     }
 8     greet() {
 9         let formatString = getFormat(this, "greeting");
10         return formatString.replace("%s", this.greeting);
11     }
12 }
13 
14 
15 function format(formatString: string) {
16     return (target: any, propertyName: string) => {
17         Reflect.defineMetadata('format', formatString, target, propertyName );
18     }
19 }
20 
21 function getFormat(target: any, propertyKey: string) {
22     return Reflect.getMetadata('format', target, propertyKey);
23 }
24 const g = new Greeter('wt');
25 console.log(g.greet());

 

属性装饰器可以被用来附加一下信息(方法是使用Reflect API),之后再使用API取得设置的信息。

五.参数装饰器

参数装饰器声明在一个参数声明之前,它可以应用于类的构造函数的参数列表或者方法的参数列表中。

参数装饰器有3个参数:

  1. 对于静态成员来说,是类的构造函数,对于实例成员来说,是类的原型对象。
  2. 成员的名字(不是参数名称,是函数名称),当是构造函数中的参数时,为undefined,这是因为class被解析成匿名函数了。
  3. 参数在函数参数列表中的索引(通过索引可以取得是函数中的哪个参数)
 1 class Greeter {
 2     greeting: string;
 3 
 4     constructor(message: string) {
 5         this.greeting = message;
 6     }
 7 
 8     @validate
 9     greet(@required name: string) {
10         return "Hello " + name + ", " + this.greeting;
11     }
12 }
13 const requiredMetadataKey = Symbol("required");
14 
15 function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
16     let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
17     existingRequiredParameters.push(parameterIndex);
18     Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
19 }
20 
21 function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) {
22     let method = descriptor.value;
23     descriptor.value = function () {
24         let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
25         if (requiredParameters) {
26             for (let parameterIndex of requiredParameters) {
27                 if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
28                     throw new Error("Missing required argument.");
29                 }
30             }
31         }
32 
33         return method.apply(this, arguments);
34     }
35 }

@required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。

使用

使用装饰器来bind容器

 1 import 'reflect-metadata';
 2 
 3 type Tag = string;
 4 type Constructor<T = any> = new (...args: any[]) => T;
 5 type BindValue = string | Function | Constructor<any>;
 6 
 7 export class Container {
 8     public bindTags: any = {};
 9     public bind(tag: Tag, value: BindValue) {
10         this.bindTags[tag] = value;
11     }
12     public get<T>(tag: Tag): T {
13         const target = this.getTagValue(tag) as Constructor;
14         const providers: BindValue[] = [];
15         for(let i = 0; i < target.length; i++) {
16             // 获取参数的名称
17             const providerKey = Reflect.getMetadata(`design:paramtypes`, target)[i].name;
18             // 把参数的名称作为Tag去取得对应的类
19             const provider = this.getTagValue(providerKey);
20             providers.push(provider);
21         }
22         return new target(...providers.map(p => new (p as Constructor)()))
23     }
24     private getTagValue(tag: Tag): BindValue {
25         const target = this.bindTags[tag];
26         if (!target) {
27             throw new Error("Can not find the provider");
28         }
29         return target;
30     }
31 }
32 export const Injectable = (constructor: Constructor) => {
33     container.bind(constructor.name, constructor);
34 }
35 export const container = new Container();
 1 import { Injectable } from "./container";
 2 
 3 @Injectable
 4 export class Hand {
 5     public hit() {
 6         console.log('Cust!');
 7         return 'Cut!';
 8     }
 9 }
10 @Injectable
11 export class Mouth {
12     public bite() {
13         console.log('Hit!');
14         return 'Hit!';
15     }
16 }
17 @Injectable
18 export class Human {
19     private hand: Hand;
20     private mouth: Mouth;
21     constructor(
22         hand: Hand,
23         mouth: Mouth
24     ) {
25         this.hand = hand;
26         this.mouth = mouth;
27     }
28     public fight() {
29         return this.hand.hit();
30     }
31     public sneak() {
32         return this.mouth.bite();
33     }
34 }
1 import 'reflect-metadata';
2 
3 import { container } from "./container";
4 import { Human } from "./test-class";
5 const human = container.get<Human>(Human.name);
6 
7 human.fight();
8 human.sneak();

 

可以看到,这里使用了Injectable类装饰器标记了类是可以注入的。

 

posted @ 2021-01-08 16:11  JasonWangTing  阅读(1007)  评论(0编辑  收藏  举报