koa下實現路由自動註冊與參數綁定

在koa下實現路由註冊與參數綁定,咱們要達到下面的效果:git

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};
    }
}

 

首先咱們須要幾個裝飾器,分別做用於類,方法和參數github

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模塊的同步方法。express

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

// bootstrap.ts

import {Application} from './application';

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

 

最終效果如圖:app

 源碼地址: github地址dom

相關文章
相關標籤/搜索