前言 最近有个项目需要实现一个简单的用于转发服务端, 调研过后决定使用Egg.js
来开发, 它是基于Koa
又封装了一层支持插件和多进程的企业级框架, 可以减少很多初期的踩坑成本
在启动好初步的框架后, 面临的最初两个问题就是
没有很方便的统一返回码和错误处理方案
如果一个接口同时支持body
和query-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 ) { 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 首先实现一个最简单的, 将body
和query-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 ) { ctx.args = Object .assign({}, ctx.request.query, ctx.request.body, ctx.request.form); return await next(); }; };
wrapper 这个中间件会复杂一点, 需要捕获内部调用的报错, 并提供统一的结构返回给前端. 而正常的返回值也需要一层填充
这里后续有个优化的部分, 在此处直接使用返回值数组解析了code
和msg
, 实际上可以封装一个class
来处理这种情况就行, 如果需要返回特殊的code
和msg
, 判断返回值是否这个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(); 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' ), ); } }, };