在koa中使用装饰器

在使用 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 {
/**
* 获取用户列表
*
* @param {Context} ctx
* @returns
* @memberof UserController
*/
@router.get('/user/getUserListByGroup')
@validateQuery({
group: Joi.number()
.required()
.error(new Error('用户组不能为空')),
})
async getUserListByGroup(ctx: Context) {
return getUserInfoList(ctx.params);
}

/**
* 修改用户信息
*
* @param {Context} ctx
* @returns
* @memberof UserController
*/
@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
* @class Router
*/
export default class Router {
// 用于存储路由信息
static routerSet: Set<{
method: string,
path: string,
middlewares: Koa.Middleware[],
}> = new Set();

/**
* 初始化路由
*
* @static
* @returns
* @memberof Router
*/
static init() {
// 加载所有控制器
glob.sync(join(__dirname, '../controller/**/*.js')).forEach(require);

// 挂载路由
for (const { method, path, middlewares } of this.routerSet) {
router[method](path, ...middlewares);
}

// 404
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');
}

/**
* 路由装饰器
*
* @param {string} path 路径
* @param {string} method 方法
* @returns
*/
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'));
}

/**
* 数据验证中间件,使用joi
*
* @param {*} schema joi chema
* @param {string} [type='query']
* @returns
*/
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();
};
}

/**
* Content-Type验证
*
* @export
* @param {string} contentType Content-Type
* @returns
*/
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();
});
}

/**
* 中间件装饰器
*
* @param {Middleware} mw
* @returns
*/
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() {}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

上面代码中,外层修饰器@dec(1)先进入,但是内层修饰器@dec(2)先执行。

完整代码:https://github.com/zubincheung/koa-ts

参考文档:https://segmentfault.com/a/1190000004357419