装饰器实现路由

src/主目录下,新建目录controller,并创建两个文件LoginController.tsdecorators.ts

decorators.ts文件中,在方法的装饰器上定义元数据,值为路由的链接:

import "reflect-metadata";
import { Router } from "express";

export const router = Router();

export function controller(target: any) {
  // 类构造器拿到的是构造函数
  for (let key in target.prototype) {
    const path = Reflect.getMetadata("path", target.prototype, key);
    const hander = target.prototype[key];
    if(path){
      router.use(path,hander)
    }
  }
}

export function get(path: string) {
  // 方法装饰器拿到的是原型对象
  return function (target: any, key: string) {
    Reflect.defineMetadata("path", path, target, key);
  };
}

LoginController.ts中,设置构造器,也可以将controller构造器设为工厂构造器,依次可配置每个请求地址的前缀:

@controller
class LoginController {
    @get("/")
    home(req: Request, res: Response) {
        const isLogin = req.session ? req.session.login : false;
        if (isLogin) {
            res.send('...');
        } else {
            res.send('...');
        }
    }
}

多种请求方法

除了基本的get方法,还有post装饰器,解决方法很简单,可以在定义元数据时,多一个对方法的定义:

export function post(path: string) {
  return function (target: any, key: string) {
    Reflect.defineMetadata('path', path, target, key);
    Reflect.defineMetadata('method', 'post', target, key);
  };
}

而在controller装饰器中,只需要对方法类型做一下判断即可:

enum Method {
  get = "get",
  post = "post",
}

export function controller(target: any) {
    // ...
    const method:Method = Reflect.getMetadata("method", target.prototype, key);
    if (path && method) {
        router[method](path, hander);
    }
}

注意上述代码中的Method类型,对请求方法做一个枚举,因为getMetadata获取的数据是any类型,而在router[method]表达式中需要字符串类型,这是为了更好的类型推断;

如果在此基础上,还需要引入deleteput等方法的装饰器,那就还需要再复制粘贴代码进行改动,这未免显得太不够优雅,我们可以定义一个函数进行统一创建:

const getRequestDecorator = (method: Method) => {
  return function (path: string) {
    return function (target: any, key: string) {
      Reflect.defineMetadata("path", path, target, key);
      Reflect.defineMetadata("method", method, target, key);
    };
  };
};

export const get = getRequestDecorator(Method.get);

装饰器添加中间件

完成上述的操作后,原先的爬虫程序就只剩下爬取数据和展示数据的接口没有迁移了,我们可以注意到,这两个接口其实与登录并无关系,那么可以新建CrowllerController.ts;除此之外,这两个接口与之前的接口不通之处在于,它们使用了自定义的中间件来检验是否已登录;这里我们可以使用use装饰器对中间件进行处理:

export function use(middleware: RequestHandler) {
    return function (target: any, key: string) {
        Reflect.defineMetadata("middleware", middleware, target, key);
    };
}

同样的,在方法中定义了装饰器后,我们需要去类装饰器中进行处理:

export function controller(target: any) {
    // ...
    const middleware = Reflect.getMetadata("middleware", target.prototype, key);
	// ...
    if (middleware) {
        router[method](path, middleware, hander);
    } else {
        router[method](path, hander);
    }
}

添加多个中间件

需要设置多个中间件时,只需要将中间件作为数组进行定义即可:

export function use(middleware: RequestHandler) {
  return function (target: any, key: string) {
    const middlewares:RequestHandler[] = Reflect.getMetadata("middlewares",target, key) || [];
    middlewares.push(middleware);
    Reflect.defineMetadata("middlewares", middlewares, target, key);
  };
}

controller装饰器中,只需要对数组进行展开即可:

export function controller(target: any) {
    // ...
    const middlewares:RequestHandler[] = Reflect.getMetadata("middlewares", target.prototype, key);
    if (middlewares && middlewares.length) {
        router[method](path, ...middlewares, hander);
    } else {
        router[method](path, hander);
    }
}

代码结构优化

经过上述的编码,我们可以发现很多问题,例如路由的创建其实是在decorator.ts文件中创建的,这显然与它的用途杂糅了,我们可以在根目录创建一个router.ts,并处理好引用包即可:

import { Router } from "express";

const router = Router();

export default router;

其次则可以将decorator.ts内的内容根据不同作用进行划分,新建decorator文件夹及controller.tsrequest.tsuse.ts三个文件分别对应三种装饰器,并作代码划分;

继而在decorator文件夹内,新建index.ts作为引用入口文件:

import "reflect-metadata";

export * from "./controller";
export * from "./request";
export * from "./use";

这样处理之后,目录结构会清晰很多,且导入包时也更统一,如在LoginController中导入时,直接导入目录decorator即可,会自动导入下面的index.ts文件:

import { controller, get, post } from "../decorator";