装饰器是一种特殊类型的声明,可以用来“装饰”三种类型的对象:类的属性/方法、访问器、类本身。装饰器使用@expression这种形式,expression求值后必须为一个函数,称为装饰器工厂函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。 
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。 
参数装饰器应用到构造函数。 
类装饰器应用到类。 
 
参数装饰器会在方法装饰器之前调用,对参数进行注解,外部装饰器可以拿到注解。
解析现代koa装饰器 下面来一段装饰器写法:
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
import  { Path, Get, Post, Param, Query, Body } from  'koa-decorate' ;
@Path ('/api/cat' )
class  CatController {
  @Get 
  @Path ('/info/:type' )
  getCatInfo (
      @Param ('type' ) type : string ,
      @Query ('info' ) info: string ) {
    return  { type , info }
  }
  @Post 
  @Path ('/info/:type' )
  CreateCat (
      @Param ('type' ) type : string ,
      @Body ('requestBody' ) requestBody: any ) {
    return  {
      status: 200 ,
      data: Object .assign(requestBody, { type  }),
      message: 'Created successfully...' 
    }
  }
}
export  { CatController };
这是一段TypeScript上 koa 路由类的写法,注意到在其中,使用了@Paht @Get的写法, 并且在入参中也有@PathParam('id') id: number这样的写法。这就是装饰器。其中 @Path(‘/api’)中的API是这个装饰器的入参,在这里是注解,因为这个框架通过Reflect.defineMetadata将这个入参写入到了该方法中。
Reflect.defineMetadata是一种反射的写法,保存一个对象到缓存中,通过Reflect.getMetadata来访问缓存,无需做过多操作
 
思考一下该如何实现。
上面我们一共引入了六个装饰器Path, Get, Post, Param, Query, Body, 其中path既是类装饰器又是方法装饰器,get和post是方法装饰器,且装饰在path方法装饰器之上,Param, Query, Body 是属性装饰器。考虑到装饰器的执行顺序,先是属性装饰器,再是方法装饰器,然后是类装饰器。
koa-decorate 参数装饰器 三个参数装饰器 Param, Query, Body 很简单,定义一个独一无二的Symbol类型的键,保存被修饰的参数的值和索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const  paramSymbolKey = Symbol.for("param" );
const  querySymbolKey = Symbol.for("query" );
const  bodySymbolKey = Symbol.for("body" );
const  Param = argName  =>
        return  (
            target: Object ,
            propertyKey: string  | symbol,
            argIndex: number 
        ) => {
            
            const  args =
                Reflect.getMetadata(paramSymbolKey, target, propertyKey) || {};
            
            args[argName] = argIndex;
            
            Reflect.defineMetadata(paramSymbolKey, args, target, propertyKey);
        };
    };
query 和 body 参数装饰器与 param 装饰器的内容大同小异,只不过是保存的不同的键的对象。
koa-decorate 方法装饰器 在参数装饰器保存了被修饰的参数的值和索引之后,方法装饰器就可以拿到相应的值,并处理一定的逻辑。path装饰器比较重要,这里详细说明。
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
const  routeSymbolKey = Symbol.for("route" );
const  pathSymbolKey = Symbol.for("path" );
export  const  Path = (path: string ): Function  =>
    return  (
        target: Function ,
        propertyKey: string  | symbol,
        decorator: PropertyDescriptor
    ) => {
        
         
         const  routeMethods =
             Reflect.getMetadata(routeSymbolKey, target) || [];
         routeMethods.push(propertyKey);
         Reflect.defineMetadata(routeSymbolKey, routeMethods, target);
         
         Reflect.defineMetadata(pathSymbolKey, path, target, propertyKey);
         
         const  oldMethodValue = decorator.value;
         
         decorator.value = (instance: Object  ) =>  async  (ctx, next) => 
             const  args = [];
             
             const  param = Reflect.getMetadata(
                 paramSymbolKey,
                 target,
                 propertyKey
             );
             
             param &&
                 Object .keys(param).map(
                     key => (args[param[key]] = ctx.params[key])
                 );
            
             const  query = Reflect.getMetadata(
                 querySymbolKey,
                 target,
                 propertyKey
             );
             query &&
                 Object .keys(query).map(
                     key => (args[query[key]] = ctx.query)
                 );
            
             const  body = Reflect.getMetadata(
                 bodySymbolKey,
                 target,
                 propertyKey
             );
             body &&
                 Object .keys(body).map(
                     key => (args[body[key]] = ctx.request.body)
                 );
            
             const  result = await  oldMethodValue.apply(instance, args);
             ctx.body = result;
        };
    };
};
get 和 post 方法装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const  httpMethodSymbolKey = Symbol.for("httpMethod" );
const  [Get, Post, Put, Delete, All] = [
    "get" ,
    "post" ,
    "put" ,
    "delete" ,
    "all" 
].map(method  =>
    return  (target, propertyKey ) =>  {
        
        Reflect.defineMetadata(
            httpMethodSymbolKey,
            method,
            target,
            propertyKey
        );
    };
});
koa-decorate 类装饰器 path类装饰器和方法装饰器共用一个函数,这里用来定义根路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const  rootPathSymbolKey = Symbol.for("rootPath" );
export  const  Path = (path: string ): Function  =>
    return  (
        target: Function ,
        propertyKey: string  | symbol,
        decorator: PropertyDescriptor
    ) => {
        
        if  (propertyKey === undefined  && decorator === undefined ) {
            
            Reflect.defineMetadata(rootPathSymbolKey, path, target.prototype);
        } else  {
            
        }
经过一系列装饰器的处理,我们只是保存了很多值,但是并没有使用,下面的将把这些值挂载在koa-router的Router对象上,用来实现相应的路由。
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
export  default  class  Decorator {
  private  router: Router;
  private  controller: Object ; 
  constructor  (options ) {
    const  {router, controllers} = options;
    if  (!controllers) {
      throw  new  Error ('There is no configuration properties "controllers"' );
    }
    this .router      = router || new  Router();
    this .controllers = controllers;
  }
  private  addRoutes (Controller) {
    
    const  instance     = new  Controller();
    
    const  rootPath     = Reflect.getMetadata(rootPathSymbolKey, Controller.prototype);
    
    const  routes       = Reflect.getMetadata(routeSymbolKey, Controller.prototype);
    routes.map((routeName: string  ) =>  {
    
      const  method     = instance[routeName];
      
      const  httpMethod = Reflect.getMetadata(httpMethodSymbolKey, Controller.prototype, routeName);
       
      const  path       = Reflect.getMetadata(pathSymbolKey, Controller.prototype, routeName);
      
      this .router[httpMethod](`${rootPath} ${path} ` , method(instance));
    });
  }
};
new  Decorator(new  Router(), CatController);
装饰器通过参数注解以Reflect.defineMetadata在缓存中形成一种配置,通过Reflect.getMetadata拿到配置,实现相应的功能。
参考文献: koa-decorate