Egg和koa中间件分析

前言

最近有个项目需要实现一个简单的用于转发服务端, 调研过后决定使用Egg.js来开发, 它是基于Koa又封装了一层支持插件和多进程的企业级框架, 可以减少很多初期的踩坑成本

在启动好初步的框架后, 面临的最初两个问题就是

  1. 没有很方便的统一返回码和错误处理方案
  2. 如果一个接口同时支持bodyquery-string, 取值方式会不一样

经过对koa的初步研究之后, 决定使用中间件来实现这两个功能

koa中间件原理

github上翻一下koa的源码, 发现它的内部实现极其简单, 只有四个类型的封装, 而对request的处理在一个非常简单的函数里就完成了

1
2
3
4
5
6
7
8
9
10
11
12
class Application extends Emitter {
// ...
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = (err) => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// ...
}

看完相关资料之后才发现, koa的所有其他功能都是基于中间件做的, 比如koa-router就是在中间件中拦截匹配到的路径, koa-bodyparser是解析body的插件等

koa的中间件是用compose函数来实现中间件调用和分发的, 仔细分析源码后发现实现非常巧妙, 通过闭包和二次封装实现了中间件顺序调用和传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function compose(middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!');
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
}

return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}

开发中间件

merge-request-args

首先实现一个最简单的, 将bodyquery-string上来的参数统一到一起, 并以body优先

不过这里还是存在一个问题, 因为ctx.params是使用koa-router解析的, 而koa-router加载在此中间件之后, 所以在此中间件无法取到ctx.params的值, 尽量避免这种使用方式就行
1
2
3
4
5
6
7
module.exports = (options) => {
return async function mergeRequest(ctx, next) {
// in this middleware cant find ctx.params
ctx.args = Object.assign({}, ctx.request.query, ctx.request.body, ctx.request.form);
return await next();
};
};

wrapper

这个中间件会复杂一点, 需要捕获内部调用的报错, 并提供统一的结构返回给前端. 而正常的返回值也需要一层填充

这里后续有个优化的部分, 在此处直接使用返回值数组解析了codemsg, 实际上可以封装一个class来处理这种情况就行, 如果需要返回特殊的codemsg, 判断返回值是否这个class实例即可
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
async function wrapper(ctx, next) {
try {
let result = await next();
// !! 接口不要直接返回array, 会造成这里判断有误
if (!ctx.body && !_.isUndefined(result)) {
if (_.isArray(result)) {
let [_result, code = 200, msg = ''] = result;
ctx.body = ctx.wrap({ data: _result, code, msg });
} else {
ctx.body = ctx.wrap({ data: result });
}
}
return result;
} catch (e) {
if (!_.isError(e)) {
e = new Error(e);
}
ctx.logger.error(e);
if (!ctx.res.finished) {
result = ctx.wrap({ success: false, code: 500, msg: _.toString(e.message || e) });
if (ctx.app.config.env !== 'prod') {
result.stack = e.stack || undefined;
}
ctx.body = result;
ctx.status = 500;
}
}
}

扩展 ctx

abort

在一般异常情况下, 可以直接使用throw new Error('msg')来返回异常信息, 但是如果需要特殊指定code和附带其他信息的时候, throw使用就不太方便了, 于是提供一个通用的ctx.abort方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
abort(code = 500, error = '', data = null) {
this.status = code;
if (code === 404) {
this.res.end();
} else {
this.res.end(
JSON.stringify({
success: false,
errorcode: code,
msg: error,
data: data,
time: Date.now(),
trace_id: this.getTraceId(),
}),
);
}
this.res.destroy();
throw new Error(_.isString(error.msg) ? error.msg : JSON.stringify(error.msg));
},
};

validate

egg官方推荐的是使用egg-validate插件来验证输入值是否正常, 但是此插件不会将验证错误的原因返回给前端, 于是封装ctx.validate用于字段校验

1
2
3
4
5
6
7
8
9
10
module.exports = {
validate(rules, args, msg = `Validation Failed`) {
const errors = this.app.validator.validate(rules, args);
if (_.size(errors)) {
throw new Error(
[msg, ...errors.map(({ field, code, message }) => `[${field}] value [${_.get(args, field, '')}] is error:[${code}] ${message}`)].join('\n'),
);
}
},
};
作者

Mosby

发布于

2018-07-27

许可协议

评论