Javascript 中的装饰器(Decorator)是我很是喜欢的一个特性,它能够很好地提升代码的复用性和自解释性。虽然它目前还处在建议征集的第二阶段,但在 TypeScript 里已经作为了一项实验性特性予以支持。javascript
好比,咱们能够用以下方式定义 Controller:html
@Controller('/cats') class CatsController { @Get() findAll(): string { return 'This action returns all cats'; } @Get('/:id') findOne(): string { return 'This action returns a specified cat'; } }
若是熟悉 Spring Boot,会以为这样的定义很是亲切。咱们使用了 @Controller
和 @Get
装饰器,表示调用 /cats
返回全部的猫,调用 /cats/:id
返回按 id 查找的猫。这样的定义形式让代码看上去可读性很强,也清爽多了。java
实际上这种写法在 TypeScript 中是比较常见的,好比 NestJs 框架就提供这种方式。node
本文简单介绍如何使用装饰器和反射实现这种功能。git
在此以前,咱们先回顾一下装饰器的用法。装饰器能够被附加到 类声明(Class),属性(Property), 访问符(Accessor),方法(Method)或 参数(Parameter) 上,对应的签名以下(其中访问符和属性装饰器签名相同):github
它们分别能够标注到对应的位置:typescript
@classDecorator // 类装饰器 class Hero { @propertyDecorator // 属性装饰器 name: string = ""; @propertyDecorator _hp: number = 100; @methodDecorator // 方法装饰器 attack(@paramDecorator enemy: Enermy /* 参数装饰器 */) { } @propertyDecorator // 访问符装饰器 get hp() { return this._hp; } }
装饰器被调用时,第一个参数通常要么能拿到类的构造函数,要么能拿到类的原型对象,利用这个参数能够对类或者原型对象进行修改。express
Reflect
对象是 ES6 为了操做对象而提供的新 API,这里须要用到的是其中的 Metadata API
,它是 ES7 的一个提案,主要用来在声明的时候添加和读取元数据。咱们主要用到 defineMetadata
定义元数据、 hasMetadata
判断元数据是否存在 和 getMetadata
获取元数据。具体函数签名见 Metadata Proposal。app
要使用 Metadata API,咱们须要引用 reflect-metadata 这个库。框架
因而咱们如今手上有两样工具,一个是装饰器,当咱们使用 @Controller
、@Get
、@Post
等标注在类或方法上时,咱们能够获取到类的构造函数、类的原型对象,根据装饰器传入的参数,能获取到路由的路径和请求方法。
但咱们还需使控制器能够运行,这时就能够利用反射,拿到装饰器传入的参数和对应的请求方法,构造出对应的路由。
这里以 Express 框架为例,咱们实现对应的装饰器,让 Express 能够支持装饰器标注来添加路由,首先新建 index.ts
以下:
import * as express from 'express'; import { Request, Response } from 'express'; const app = express(); app.get('/', (req: Request, res: Response) => { res.send('Hello World!'); }); app.listen(3000, () => { console.log('Started express on port 3000'); });
这是一个基础的 Express 入口文件。
@Controller
装饰器接下来实现 @Controller
装饰器,它是标注在控制器 类 上的,用来标注这个类是一个控制器类,并提供一个路由前缀做为参数。由于类装饰器第一个参数是类的构造函数,因此咱们将该装饰器传入的前缀参数定义到构造函数的元数据中,key 为 prefix
。
// Controller.ts export const Controller = (prefix: string = ''): ClassDecorator => { return (target: Function) => { Reflect.defineMetadata('prefix', prefix, target); }; };
@Get
装饰器@Get
、@Post
等做为请求方法的装饰器实现原理都是类似的,这里以 @Get
方法举例,这个装饰器应该标识请求的方式和请求的路由,另外保存被标注的函数,由于这个函数将被做为路由函数调用。
咱们首先定义一个元数据接口:
// RouteDefinition.ts export interface RouteDefinition { path: string; requestMethod: 'get' | 'post' | 'delete' | 'options' | 'put'; methodName: string; }
@Get
装饰器的实现以下:
// Get.ts import {RouteDefinition} from './RouteDefinition'; export const Get = (path: string): MethodDecorator => { return (target, propertyKey: string): void => { if (!Reflect.hasMetadata('routes', target.constructor)) { Reflect.defineMetadata('routes', [], target.constructor); } const routes = Reflect.getMetadata('routes', target.constructor) as Array<RouteDefinition>; routes.push({ requestMethod: 'get', path, methodName: propertyKey }); Reflect.defineMetadata('routes', routes, target.constructor); }; };
@Get
装饰器是标注在方法上的,因此第一个参数是类的原型对象,咱们这里仍是根据它再获取到类的构造函数,在元数据中添加一个 routes
数据,用来保存这个控制器的全部路由。
最后,咱们在 Express 的入口文件中,就能够取得全部的控制器,根据反射拿到全部的路由了。
import 'reflect-metadata'; import * as express from 'express'; import { Request, Response } from 'express'; import CatsController from './CatsController'; import { RouteDefinition } from './RouteDefinition'; const app = express(); app.get('/', (req: Request, res: Response) => { res.send('Hello there!'); }); app.listen(3000, () => { console.log('Started express on port 3000'); }); // 构造路由 [ CatsController ].forEach(controller => { const instance = new controller(); // 获取 prefix const prefix = Reflect.getMetadata('prefix', controller); // 获取 routes const routes: Array<RouteDefinition> = Reflect.getMetadata('routes', controller); routes.forEach(route => { // 添加 Express 路由 app[route.requestMethod](prefix + route.path, (req: express.Request, res: express.Response) => { instance[route.methodName](req, res); }); }); });