在使用 koa 开发的过程中,经常会忘记把 controller 的方法加到 router 中去,期望使用 decorator 实现路由配置及一些参数校验。
示范代码均采用 TypeScript,实现效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| export default class UserController {
@router.get('/user/getUserListByGroup') @validateQuery({ group: Joi.number() .required() .error(new Error('用户组不能为空')), }) async getUserListByGroup(ctx: Context) { return getUserInfoList(ctx.params); }
@router.post('/user/setUserInfo') @allow('json') @validateBody({ userId: Joi.number().required(), group: Joi.number().required(), }) async setUserInfo(ctx: Context) { return updateUserInfo({ group: ctx.params.group }, { userId: ctx.params.userId }); } }
|
router 装饰器
首先要定义一个 Router 类,使用 routerSet 存储路由信息,在 init 方法加载所有控制器和挂载路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
export default class Router { static routerSet: Set<{ method: string, path: string, middlewares: Koa.Middleware[], }> = new Set();
static init() { glob.sync(join(__dirname, '../controller/**/*.js')).forEach(require);
for (const { method, path, middlewares } of this.routerSet) { router[method](path, ...middlewares); }
router.all('*', (ctx: Koa.Context) => { ctx.status = 404; ctx.error('Router Not Found'); });
return router; } }
|
实现装饰器,把路由信息和处理函数保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export function get(path: string) { return addRouterDecorator(path, 'get'); }
function addRouterDecorator(path: string, method: string) { assert( typeof method === 'string' && typeof path === 'string', 'method and path should be string', );
return (target: any, name: string, descriptor: PropertyDescriptor) => { Router.routerSet.add({ method: method, path, middlewares: toArray(Reflect.get(target, name)), });
return descriptor; }; }
|
koa 中间件装饰器
普通的 koa 中间件装饰器则更为简单,不需额外的存储挂载过程,直接定义就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| export function validateQuery(schema) { return middlewareDecorator(ValidateMW(schema, 'query')); }
export function validateBody(schema) { return middlewareDecorator(ValidateMW(schema, 'body')); }
function ValidateMW(schema: any, type: string = 'query') { assert(!isEmpty(schema), 'schema is empty'); assert(isObject(schema), 'schema should be object');
return async function(ctx: Context, next: Function) { const { error, value } = Joi.validate(ctx.request[type], schema);
if (error) { throw new CWErrors(error.message, errCodeEnum.paramTypeError); }
ctx.params = { ...ctx.params, ...value };
return next(); }; }
export function allow(...contentTypes: string[]) { assert(contentTypes.length > 0, 'ContentType is empty');
return middlewareDecorator((ctx: Context, next: Function) => { if (!ctx.is(contentTypes)) { throw new CWErrors('不支持当前表单类型'); } return next(); }); }
function middlewareDecorator(mw: Middleware) { return function(target: any, name: string, descriptor: PropertyDescriptor) { const values = toArray(mw, Reflect.get(target, name)); Reflect.set(target, name, values); return descriptor; }; }
|
注意:如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。所以中间件需要添加到数组的开头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function dec(id) { console.log('evaluated', id); return (target, property, descriptor) => console.log('executed', id); }
class Example { @dec(1) @dec(2) method() {} }
|
上面代码中,外层修饰器@dec(1)先进入,但是内层修饰器@dec(2)先执行。
完整代码:https://github.com/zubincheung/koa-ts
参考文档:https://segmentfault.com/a/1190000004357419