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地址