koa下实现路由自动注册与参数绑定

在koa下实现路由注册与参数绑定,我们要达到下面的效果:

import {Controller, RequestBody, RequestMapping, RequestParam} from '../decorator/RouterDecrator';
import {LoggerFactory} from '../util/logger';
import {timeCounter} from '../middlewares/TimeCounter';
import {User} from '../domain/User';

const logger = LoggerFactory.getLogger('LeadController');

@Controller('/user', [timeCounter])
export default class UserController {

    @RequestMapping({path: '/get', method: 'get'})
    public async getUser (@RequestParam('id') userId: number){
        return {id: userId, name: 'test'};
    }

    @RequestMapping({path: '/add', method: 'post'})
    public async addUer (@RequestParam('token') token: string, @RequestBody() user: User){
        logger.info('UserController.addUer');
        return {token, user};
    }
}

 

首先我们需要几个装饰器,分别作用于类,方法和参数

import {NextFunction} from 'express';
import {Context} from 'koa';

export const REQUEST_BODY = 'RequestBody';

export type MiddleWare = (context: Context, next: NextFunction) => void;

/**
 * 各个装饰器在类的原型上添加数据
 * path+subPath 完整路径
 * method 请求方法get,post等
 * middleWares 中间件
 */

// 类装饰器
export function Controller (path= '/', middleWares?: MiddleWare[]) {
    return (target: any) => {
        target.prototype.path = path;
        target.prototype.middleWares = middleWares;
    };

}

// 方法装饰器
export function RequestMapping (config: {path: string, method: string,
                                         middleWares?: MiddleWare[]}) {

    return (target: any, name: string, descriptor: PropertyDescriptor) => {
        target[name].subPath = config.path;
        target[name].requestMethod = config.method;
        target[name].middleWares = config.middleWares;
    };

}

// 参数装饰器
export function RequestParam (paramName: string) {
    return (target: any, methodName: string, index: number) => {

        const  params = target[methodName].paramList || {};
        params[paramName] = index;
        target[methodName].paramList = params;
    };
}

// 参数装饰器
export function RequestBody () {
    return (target: any, methodName: string, index: number) => {

        const  params = target[methodName].paramList || {};
        params[REQUEST_BODY] = index;
        target[methodName].paramList = params;

    };
}

 

接下来,需要对koa提供的类进行包装,将路由注册之后,再暴露给外部。此外,由于方法装饰器和类装饰器在类被加载的时候才会生效,所以需要加载所有的controller类,这是用了fs模块递归加载。同时由于这个方法只在启动时调用一次,所以可以调用fs模块的同步方法。

import Koa, {Context} from 'koa';
import Router from 'koa-router';
import {MiddleWare, REQUEST_BODY} from './decorator/RouterDecrator';
import * as path from 'path';
import * as fs from 'fs';
import bodyParser from 'koa-bodyparser';
import {LoggerFactory} from './util/logger';
import {responseMethod} from './middlewares/ResHandle';

const logger = LoggerFactory.getLogger('Application');
export class Application {

    private app: Koa;
    private globalRouter: Router;

    constructor () {
        this.app = new Koa();
        this.globalRouter = new Router();
        this.app.on('error', (err) => {
            throw err;
        });
        this.app.use(bodyParser());

        this.app.use(responseMethod);

        this.loadControllers(path.join(__dirname, './controller'));

        this.app.use(this.globalRouter.routes());
    }

    // 递归加载controller目录下的ts文件
    private loadControllers (filePath: string): void{
        const files = fs.readdirSync(filePath);
        files.forEach((file) => {
            const newFilePath = path.join(filePath, file);
            if (fs.statSync(newFilePath).isDirectory()){
                this.loadControllers(newFilePath);
            }else{
                const controller = require(newFilePath);
                this.registerRouters(controller);
            }
            }
        );
    }

    // 注册路由
    private registerRouters (controller: any): void{
        if (!controller){
            return;
        }

        const proto = controller.default.prototype;
        const prefix = proto.path;
        const middleWares: MiddleWare[] = proto.middleWares;

        const properties = Object.getOwnPropertyNames(proto);

        properties.forEach((property) => {
            if (proto[property] && proto[property].subPath){
                const fullPath = (prefix + proto[property].subPath).replace(/\/{2,}/g, '/');
                const method = proto[property].requestMethod;

                // 累加中间件
                const fullMiddleWares: MiddleWare[] = [];
                if (middleWares){
                    fullMiddleWares.concat(middleWares);
                }
                if (proto[property].middleWares){
                    fullMiddleWares.concat(proto[property].middleWares);
                }

                const router = new Router();
                logger.info(`add url:${fullPath}`);
                const  asyncMethod = async (context: Context) => {

                    const paramList = proto[property].paramList;
                    const args: any = [];
                    if (paramList) {

                        // 参数绑定
                        const paramKeys = Object.getOwnPropertyNames(paramList);
                        paramKeys.forEach((paramName) => {
                            const index = paramList[paramName];
                            args[index] = paramName === REQUEST_BODY ?
                                JSON.parse(JSON.stringify(context.request.body)) : context.query[paramName];
                        });
                    }
                    context.body = await proto[property].apply(proto, args);

                };

                // 添加中间件
                if (middleWares){
                    router.use(...middleWares);
                }
                router[method](fullPath, asyncMethod);
                this.globalRouter.use(router.routes());
                this.globalRouter.use(router.allowedMethods());
        }
        });

    }

    public listen (port: number){
        this.app.listen(port);
    }

}

 

最后,写一个入口文件启动服务

// bootstrap.ts

import {Application} from './application';

const app = new Application();
app.listen(3000);

 

最终效果如图:

 源码地址: github地址

posted @ 2019-03-25 22:07  gluawwa  阅读(1127)  评论(0编辑  收藏  举报