在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