Javascript中的装饰器(Decorator)是我非常喜欢的一个特性,它可以很好的提高代码的复用性和自解释性。尽管它目前处于提案征集的第二阶段,但它作为TypeScript中的一项实验性功能得到了支持。例如,我们可以这样定义一个Controller:@Controller('/cats')classCatsController{@Get()findAll():string{return'Thisactionreturnsallcats';}@Get('/:id')findOne():string{return'这个动作返回指定的猫';如果你熟悉SpringBoot,你会发现这个定义非常友好。我们使用@Controller和@Get装饰器来指示调用/cats返回所有猫,调用/cats/:id返回通过id查找的猫。这种形式的定义使代码看起来非常可读并且更清晰。其实这种写法在TypeScript中很常见,比如NestJs框架就提供了这种写法。本文简单介绍一下如何使用装饰器和反射来实现这个功能。装饰器概述在此之前,我们先来回顾一下装饰器的用法。装饰器可以附加在类声明(Class)、属性(Property)、访问器(Accessor)、方法(Method)或参数(Parameter)上,对应的签名如下(其中访问器和属性的签名decorator相同):可以标注到相应的位置:@classDecorator//类装饰器classHero{@propertyDecorator//属性装饰器名称:string="";@propertyDecorator_hp:number=100;@methodDecorator//方法装饰器attack(@paramDecoratorenemy:Enermy/*参数装饰器*/){}@propertyDecorator//访问器装饰器gethp(){returnthis._hp;}}调用装饰器时,通常可以得到第一个参数类的构造函数既可以得到类的原型对象,也可以通过这个参数来修饰类或原型对象。Reflect对象是ES6提供的一种新的操作对象的API。这里需要用到的是MetadataAPI,它是ES7的一个提案,主要用于声明时添加和读取元数据。我们主要使用defineMetadata定义元数据,hasMetadata判断元数据是否存在,getMetadata获取元数据。具体函数签名见元数据提案。要使用元数据API,我们需要引用reflect-metadata库。思路所以我们现在手上有两个工具,一个是装饰器,当我们使用@Controller、@Get、@Post等标记一个类或方法时,我们可以得到类的构造函数,类的原型对象类,根据装饰器传入的参数可以得到路由路径和请求方法。但是我们仍然需要使控制器运行。这时候我们可以通过反射获取装饰器传入的参数和对应的请求方法,构造对应的路由。实现以Express框架为例,我们实现相应的装饰器,让Express支持装饰器注解添加路由,首先新建一个index.ts如下:import*asexpressfrom'express';import{Request,Response}from'express';constapp=express();app.get('/',(req:Request,res:Response)=>{res.send('HelloWorld!');});app.listen(3000,()=>{console.log('Startedexpressonport3000');});这是一个基本的Express条目文件。@Controller装饰器接下来实现@Controller装饰器,在控制器类上进行标记,将类标记为控制器类,并提供路由前缀作为参数。因为类装饰器的第一个参数是类的构造函数,所以我们将装饰器传入的prefix参数定义到构造函数的元数据中,key为prefix。//Controller.tsexportconstController=(prefix:string=''):ClassDecorator=>{return(target:Function)=>{Reflect.defineMetadata('prefix',prefix,target);};};@Get装饰器@Get、@Post等装饰器作为请求方法的实现原理是类似的。这里以@Get方法为例。这个装饰器应该识别请求的请求方法和路由,并保存标记的函数,因为这个函数将作为路由函数被调用。我们首先定义一个元数据接口://RouteDefinition.tsexportinterfaceRouteDefinition{path:string;请求方法:'获取'|'发布'|'删除'|'选项'|'放';methodName:string;}@Getdecoration装饰器的实现如下://Get.tsimport{RouteDefinition}from'./RouteDefinition';exportconstGet=(path:string):MethodDecorator=>{return(target,propertyKey:string):void=>{if(!Reflect.hasMetadata('routes',target.constructor)){Reflect.defineMetadata('routes',[],target.constructor);}constroutes=Reflect.getMetadata('routes',target.constructor)asArray
