Nodejs | 2021-03-15 23:20:27 2654次 2次
koa 是一个简易实用的框架,尤其是中间件的设计思想,我并说不出哪里好或者不好,因为我没有实际的使用。大致体会下,就是上一个中间件可以控制下一个,但是顺序紧密结合,从而导致的问题是没有一个好的流程跳转。
当然,这个中间件设计思路对我的影响是有意义的,比如在写业务的时候,设计了一个事件模型,假如有一个事件,在基础代码中预置了逻辑,暴露给业务使用时,提供一个此事件前、后回调并且可以决定是否执行预置逻辑,那么就可以通过 next 机制实现,伪代码如下:
// async or common Save(next, props){ // before next() // after }
在了解 koa 这个主要的思想之后,下面看下它的大致流程,文件结构划分很简单,只有四个文件,代码量也很少:
application
context
request
response
入口 application
首先是 createServer 启动一个服务,紧接着创建 createContext 关系,再次是中间件调度执行,最后做出响应,按照这个步骤可以实现一套简单的代码:
let http = require('http'); let context = require('./context'); let request = require('./request'); let response = require('./response'); class Koa{ constructor(){ this.context = context; this.request = request; this.response = response; this.middlewares = []; } use(cb){} createContext(req, res){ // 多个示例 避免数据共享 let ctx = Object.create(this.context); ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; } compose(ctx, middlewares){} handleRequest(req, res) { res.statusCode = 404; let ctx = this.createContext(req, res) let composeMiddleware = this.compose(ctx, this.middlewares); //执行后ctx.body会被修改 ... } listen(...arg){ let server = http.createServer(this.handleRequest.bind(this)) server.listen(...arg) } } module.exports = Koa;
创建 context 关系时,通过 Object.create 避免多个 koa 实例中 ctx 数据共享。
关于中间件其实没那么神秘,当 use 的时候,收集中间件方法进入一个队列,通过 compose 方法来执行,之前在写 js 进阶部分中有介绍 redux 中的 compose 实现 ,但是这里的中间件执行和 redux 中是不一样的,这是一种异步洋葱模型,原理如下:
// middlewares 队列包含所有的中间件方法 compose(ctx, middlewares){ let dispatch = (index) => { // 到达最后一个 if(index === middlewares.length){ return Promise.resolve(); } let mid = middlewares[index]; // next return Promise.resolve(mid(ctx, () => dispatch(index + 1))) } return dispatch(0); }
request/response
对 http 原生方法的一层封装,源码中实现很全面,大致意思如下,:
let parse = require('parseurl'); let request = { get url(){ return this.req.url; }, get path(){ return parse(this.req).pathname; } } let response = { set body(value){ this.res.statusCode = 200; this._body = value; }, get body(){ return this._body; } }
context
这个方法主要是对 request/response 中方法的劫持,直接操作 ctx.xxx 即可,无需 ctx.response.xxx,所以思路也很清晰,大致原理如下,就是对 request/response 添加一层代理:
class Delegator{ constructor(proto, target){ this.proto = proto; this.target = target; } getters(name) { let { proto, target } = this; Object.defineProperty(proto, name, { get() { return this[target][name]; }, configurable: true }); return this; } setters(name) { let { proto, target } = this; Object.defineProperty(proto, name, { set(v) { this[target][name] = v; }, configurable: true }); return this; } } let proto = {} new Delegator(proto, 'request') .getters('url') .getters('path') new Delegator(proto, 'response') .getters('body') .setters('body') module.exports = proto;
错误机制
如上的方法中介绍了 koa 一个大致的思路,还有一个很重要的点,那就是错误处理机制。koa 类继承自 event 模块,可以监听错误;错误的处理在 application 和 context 中都有,他们的区别是前者处理的是用户没有进行 app.on('error') 时,则默认帮助监听错误,实现原理如下:
application.js: onerror(err){ //非原生错误拦截掉,否则会正常启动服务 app.onerror('some error') const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error; if (!isNativeError){ throw new TypeError('non-error thrown: ' + err) }; // 原生错误输入日志提示 const msg = err.stack || err.toString(); console.error(`\n${msg.replace(/^/gm, ' ')}\n`); } handleRequest(){ // 如果用户没有 app.on('error') 监听,进行内置的处理 if (!this.listenerCount('error')){ this.on('error', this.onerror) }; }
再去看下 context 中的错误处理,它的目的是在中间件中有错误丢出时,可以帮助你处理这个错误,并通过 event 的 emit 通知,这样也可以监听,原理如下:
let proto = { throw(...arg){ const err = new Error(); err.status = arg[0]; err.message = arg[1]; throw err; }, onerror(err) { if (null == err) return; this.app.emit('error', err, this); const { res } = this; if (typeof res.getHeaderNames === 'function') { res.getHeaderNames().forEach(name => res.removeHeader(name)); } // respond this.type = 'text/plain;charset=utf-8'; this.status = err.status; res.end(err.message); } }
测试用例:
app.use(async (ctx, next)=>{ // 通过这个方法丢出错误 ctx.throw(403, '暂无权限') await next() })
在上面的 onerror 中仅仅是根据错误作出响应,那么如何捕获到中间件中的错误,这个就需要回到 compose 方法,因为中间件中有错误(执行 ctx.throw 时丢出的),所以可以通过 try catch 进行捕获,并且在执行中间件的时候进行 catch:
compose(ctx, middlewares){ let dispatch = (index) => { ... // next try{ return Promise.resolve(mid(ctx, ()=> dispatch(index + 1))) }catch(err){ return Promise.reject(err) } } return dispatch(0); } handleRequest(req, res) { ... const onerror = err => ctx.onerror(err); // 通过 catch 调用 context 中的错误处理 this.compose(ctx, this.middlewares).then(...).catch(onerror) }
koa 的设计中主要还是围绕 response/request 的处理,从而展开的一些轻量封装。
2人赞