利用装饰器二次封装express

2,078 阅读4分钟
(转载请注明出处)

昨天啊呆同学又不好好做咖啡,请了假偷偷去面试了。下午回来时垂头丧气,一看就很不顺利的样子。

我问阿呆是不是又去找互联网工作受挫了。阿呆说,师傅你平时总是对我说学知识要广泛涉猎,不应该在一小块地方来回打转,更不要成为api复读机,只有见识广了,才能集各家之长,吸收优秀的思想。可是我去面试时,对方总会问一些api相关,比如小程序解析html片段所用到的方法,node进程守护使用的pm2的api,甚至最新框架版本号这些你认为很无聊的问题。我跟他们说这些问题都是可以网上秒查的,对方就很生气的问我不学这些还能学哪些东西。

我说我除了学前端相关,还学了c、java、ts、mysql、算法和网络相关。对方更生气了,说:你为什么不学node和mongodb,难道你认为这两个比java和mysql哪里差了吗,还是你觉得你前端知识很厉害,不需要再精进了。

阿呆越说越委屈,说一定是师傅你哪里教我错了。

我看着阿呆,等他心情平复之后说:你自己觉得你学了这么多,对你前端方面的技能有没有帮助?

阿呆说: 好像有一点儿。

我: 比如?

阿呆:我最近用node和express写接口时总觉得定义路由有点儿麻烦,每定义一个路由就要写一个路由方法, 像这样:

router.get('/getData', monitor.getMonitorData);
router.get('/deleteData', monitor.deleteMonitorData);
router.post('/errCatch', monitor.setMonitorData);

我在想如果像java的spring框架那样用注解的方式将路由定义在每一个controller上可不就省心多了, 像这样:


@RestController
@RequestMapping("/home")
public class IndexController {
    @RequestMapping("/")
    String get() {
        //mapped to hostname:port/home/
        return "Hello from get";
    }
    @RequestMapping("/index")
    String index() {
        //mapped to hostname:port/home/index/
        return "Hello from index";
    }

我: 有没有尝试一下?

阿呆:我试着用ts中的装饰器模仿了一下,我给你看看。

/* /decorator/controller.ts  这个是放在controller类上的注解。*/
import "reflect-metadata";
import {AppRouter} from "../AppRouter";
import {MetadataKey} from "./MetadataKey";
import { Methods } from './Methods';
import {NextFunction, Request, RequestHandler, Response} from "express";

/**
 * @function 参数字段校验
 * @param keys {String[]} 需要校验的字段
 */
export function bodyValidators(keys: string): RequestHandler {

    return function (req: Request, res: Response, next: NextFunction) {
        if (!req.body) {
            res.status(422).send("Invalid request");
            return;
        }
        for (let key of keys) {
            if (!req.body[key]) {
                res.status(422).send(`Missing property ${key}`);
                return;
            }
        }
        next();
    }
}

/**
 * @function 类注解
 * @param routePrefix {String} 路由前缀
 */
export function controller(routePrefix: string) {
    /**
     * @function 目标拦截
     * @param  target {Function} 当前类的构造函数
     */
    return function (target: Function) {

        // 获取express.Router
        const router = AppRouter.getInstance();

        for (let key in target.prototype) {
            // 获取构造函数上的原型成员
            const routeHandler = target.prototype[key];
            // 获取原型成员上的路由路径
            const path = Reflect.getMetadata(MetadataKey.path, target.prototype, key);
            // 获取原型成员上的方法
            const method: Methods = Reflect.getMetadata(MetadataKey.method, target.prototype, key);
            // 获取原型成员上的中间件
            const middlewares = Reflect.getMetadata(MetadataKey.middleware, target.prototype, key) || [];
            // 获取原型成员上的校验字段
            const requireBodyProps = Reflect.getMetadata(MetadataKey.validator, target.prototype, key) || [];

            // 生成校验器控件
            const validator = bodyValidators(requireBodyProps);

            if (path) { // 生成路由
               router[method](
                    `${routePrefix}${path}`,
                    ...middlewares,
                    validator,
                    routeHandler
                );
            }
        }
    }
}
/*  AppRouter.ts  */
import express from "express";

export class AppRouter {
    private static instance: express.Router;

    static getInstance (): express.Router {
        if (!AppRouter.instance) {
            AppRouter.instance = express.Router();
        }
        return AppRouter.instance;
    }
}
/*  MetadataKey  */
export enum MetadataKey {
    method = "method",
    path = "path",
    middleware = "middleware",
    validator = "validator"
}
/*  Methods  */
export enum Methods {
    get = 'get',
    post = 'post',
    patch = 'patch',
    del = 'delete',
    put = 'put'
}
/*  /decorators/bodyValidator.ts  */
import "reflect-metadata";
import {MetadataKey} from "./MetadataKey";

/**
 * @function 定义校验控件
 * @param keys {ArrayLike} 需要校验的字段
 */
export function bodyValidator(...keys: string[]) {

    return function (target: any, key: string, desc: PropertyDescriptor) {
        Reflect.defineMetadata(MetadataKey.validator, keys, target, key);
    }
}

这个是放在controller类方法上的二级路由注解

/*  routes.ts  */
import "reflect-metadata";
import {RequestHandler} from "express";
import {Methods} from "./Methods";
import {MetadataKey} from "./MetadataKey";

interface RouteHandlerDescriptor extends  PropertyDescriptor {
    value?: RequestHandler
}

/**
 * @function 路由绑定
 * @param method {String} 请求方法
 */
function routeBinder(method: string) {
    /**
     * @function 路径获取
     * @param path {String} 路由路径
     */
    return function (path: string) {
        /**
         * @function 注解
         * @param target {Object} 注解方法所在类的实例
         * @param key {String} 注解方法名
         * @param desc {RouteHandlerDescriptor} 注解方法中的描述符
         */
        return function (target: any, key: string, desc: RouteHandlerDescriptor) {

            // 定义路径(路径名,路径值, 所在类实例, 所在方法名称)
            Reflect.defineMetadata(MetadataKey.path, path, target, key);
            // 定义方法(方法名, 方法值, 所在类实例, 所在方法名称)
            Reflect.defineMetadata(MetadataKey.method, method, target, key);
        }
    }
}

export const get = routeBinder(Methods.get);
export const put = routeBinder(Methods.put);
export const post = routeBinder(Methods.post);
export const del = routeBinder(Methods.del);
export const patch = routeBinder(Methods.patch);
/*  use.ts中间件方法  */
import 'reflect-metadata';
import { RequestHandler } from 'express';
import { MetadataKey } from './MetadataKey';

/**
 * @function 中间件注解
 * @param middleware {RequestHandler} 加入中间件
 */
export function use(middleware: RequestHandler) {
    /**
     * @function 目标拦截
     * @param target {Object} 注解目标所在类的实例
     * @param key {String} 注解目标的名称
     * @param desc {Object} 注解目标的属性描述符
     */
    return function(target: any, key: string, desc: PropertyDescriptor) {
        // 获取当前目标定义在当前获取的中间件前面的中间件列表
        const middlewares = Reflect.getMetadata(MetadataKey.middleware, target, key) || [];
        // 合并定义当前目标上的所有中间件
        Reflect.defineMetadata(
            MetadataKey.middleware,
            [...middlewares, middleware],
            target,
            key
        );
    };
}

使用的时候是这样的

/*  /middlewares/requireAuth.ts  */
import {NextFunction, Request, Response} from "express";

export function requireAuth(req: Request, res: Response, next: NextFunction) {
    if (req.session && req.session.loggedIn) {
        next();
        return
    }
    res.status(403);
    res.send("Not Permitted")
}
/*  /controllers/rootControllers.ts  */
import {Request, Response, NextFunction} from "express";
import {controller, get, post, bodyValidator, use} from "../decorators";
import {requireAuth} from "../middlewares/requireAuth";


@controller("/root")
class RootControllers {

    @get("") // /root
    getRoot(req: Request, res: Response) {
        if (req.session && req.session.loggedIn) {
            res.send(` This is root router `);
        }
    }

    @get('/protected') // /root/protected
    @use(requireAuth)
    getProtected(req: Request, res: Response) {
        res.send('Welcome to protected route, logged in user');
    }

    @post('/login') // /root/login
    @bodyValidator('email', 'password')
    postLogin(req: Request, res: Response) {
        const { email, password } = req.body;

        if (email === 'hi@hi.com' && password === 'password') {
            req.session = { loggedIn: true };
            res.redirect('/');
        } else {
            res.send('Invalid email or password');
        }
    }
}

再将controller类引入到根文件就行了

import express from "express";
import bodyParser from "body-parser";
import cookieSession from "cookie-session";
import { AppRouter } from "./AppRouter";
import "./controllers/RootControllers";
const app = express();

app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieSession({keys: ["laskdjf"]}));
app.use(AppRouter.getInstance());

app.listen(3000, () => {
    console.log('Listening on port 3000');
});

师傅你觉得怎么样?阿呆问道。

我:你觉得呢?

阿呆:我觉得我还是安心做咖啡吧,互联网不适合我。

听到这句话我长舒了一口气,阿呆,你能这么想我很欣慰。

阿呆制作咖啡日常