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

浙公网安备 33010602011771号