使用裝飾器是如何構建 Nodejs 路由的

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 Proposalapp

要使用 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);
    });
  });
});

參考

相關文章
相關標籤/搜索