使用装饰器是如何构建 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);
    });
  });
});

参考

相关文章
相关标签/搜索