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); }); }); });