使用过koa的人应该都会知道koa处理请求是按照中间件的形式的。而中间件并无返回值的。那么若是一个中间件的处理结果须要下一个中间件使用的话,该怎么把这个结果告诉下一个中间件呢。例若有一个中间件是解析token的将它解析成userId,而后后面的业务代码(也就是后面的一个中间件)须要用到userId,那么如何把它传进来呢?
其实中间件就是一个函数,给函数传递数据最简单的就是给他传入参数了。因此维护一个对象ctx,给每一个中间件都传入ctx。全部中间件便能经过这个ctx来交互了。ctx即是context的简写,意义就是请求上下文,保存着此次请求的全部数据。
那么有人便会提出疑问了,request 事件的回调函数不是有两个参数:request,response吗,为何不能把数据存放在request或者response里呢?设计模式的基本原则就是开闭原则,就是在不修改原有模块的状况下对其进行扩展。想象一下,假设咱们在解析token中的中间件为request增长一个属性userId,用来保存用户id,后面的每一个业务都使用了request.userId。某一天koa升级了http模块,他的request对象里有一个userId属性,那咱们该怎么办?要是修改本身的代码,就要把全部的userId替换一下!要是直接修改http模块,那每次想要升级http模块,都要看一次http模块的新代码。html
要找到ctx定义的地方,最直接的方式就是从http.createServer(callback)里的callback中找。上一节,咱们看到koa脚手架,传入的callback就是app.callback()。而app是new了koa。因此咱们找到koa中的callback方法。json
callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; }
上面的代码即是koa中的callback方法。返回的函数handleRequest即是咱们熟悉的(req,res)=>{} 形式的。ctx是经过koa的createContext方法构建的。并将构建好的ctx传递给koa的handleRequest方法(注意不是callback方法中定义的handleRequest)。下面看下createContext方法吧设计模式
createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); request.ip = request.ips[0] || req.socket.remoteAddress || ''; context.accept = request.accept = accepts(req); context.state = {}; return context; }
a = Object.create(b)能够理解为a={},a的原型为b。 从第一句能够看到,context是一个继承了this.context的对象,this为koa实例。后面主要为context赋值一些属性。这些属性主要来自this.request,this.response,以及传进来的req,res,也就是http模块request事件的两个回调参数。req,res不用说了,接下来让咱们看下koa对象的request,response,和context属性吧。数组
const context = require('./context'); const request = require('./request'); const response = require('./response'); module.exports = class Application extends Emitter { constructor() { super(); //删去了部分代码 this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } // 删去了其余方法 }
能够看到koa中的context,request,response分别继承了引入的三个模块。下面咱们来看下这三个模块。cookie
在看context模块以前,让咱们先回顾下,context模块和咱们日常写中间件用的ctx之间的关系。
koa的脚手架的启动文件是www文件,而www先require了app文件。app中new了koa。因此这个脚手架在进行http监听以前就先new了koa,执行了koa中的构造函数。因此koa的实例app中有一个属性context继承了context模块。当有http请求进来后,会触发app的callback方法,里面调用了createContext方法,并将请求的req,res传入。 这个函数真正构建了context,也就是咱们中间件里用的ctx。context继承了app.context。还为他赋值了一些属性。ctx构建完成,就做为入参传入咱们定义的中间件中了。
有人已经看到了,ctx没有咱们经常使用的一些属性啊,咱们常常用ctx.url,ctx.method,ctx.body等属性都尚未定义。剩下惟一能影响ctx的就是context模块了,由于ctx继承app.context,app.context继承了context模块。
看过context模块的源码后你会发现,里面定义了proto = module.exports。proto这个对象就是context模块暴露出的对象。这个对象仅仅定义了5个方法,还不是咱们经常使用的。其实后面还有两句,其中一句是:app
delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable');
delegate是引入的一个模块。上一句的功能就是将proto.response的一些属性方法赋给proto,下面详细看下method方法。koa
function Delegator(proto, target) { if (!(this instanceof Delegator)) return new Delegator(proto, target); this.proto = proto; this.target = target; this.methods = []; this.getters = []; this.setters = []; this.fluents = []; } Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; this.methods.push(name); proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; return this; };
Delegator这个对象有两个属性,proto和target。对应刚刚的proto和'response'。调用method便能将response的方法委托给proto。method接受一个参数name,也就是要委托的方法的key值。method方法中最重要的一句就是proto[name]=function(){},为proto新增了一个方法,方法的内容就是调用proto.target[name]。这里用了apply,这个apply的目的不是为了改变this的指向,是为了将传给proto[name]方法的参数,原封不动的传给proto.target[name]方法。
Delegator还有三个方法,getter,setter,access。getter就是调用了proto.defineGetter。setter就是调用了proto.defineGetter。具体用法再也不赘述。access就是调用了getter方法和setter方法。
因此咱们能够看到ctx大部分方法和属性都是委托给了response和request。那response和request是谁呢?这个要看ctx.request和ctx.response是谁了。对于咱们在中间件所引用的那个ctx,经过上面的流程图能够看到,经过createContext方法,为他赋值了request和response。经过看createContext这个方法的源码,能够看到这两个对象与request模块,response模块的关系如同context模块和ctx。都是通过了两次继承。socket
这连个模块的功能分别就是封装了http的请求和响应。那http模块已经封装好了http的请求和响应了,为何koa还要搞出来request模块和response模块呢?答案就是为了扩展和解耦。
先说扩展:例如koa要是直接让咱们使用http封装的response,咱们要想在响应体中返回一个json,就要设置header中的context-type为json,想返回buffer就要设置成bin,还要设置响应码为200。而koa封装好的ctx.body方法呢(其实调用的是response.body)只须要将响应值给他,他本身经过判断是否响应值为空来设置http状态码,经过响应值的类型来设置header中的context-type。
再说解耦:那response和request模块是如何实现的这些功能呢?其实也是经过的http封装的req,res。那为何不能直接使用req和res呢?首先koa想要扩展他们的功能,若是直接为他们添加方法那就违法了前面说过的开闭原则。还有就是解耦。让咱们(koa的用户)的代码和http模块无关。这样koa某天若是以为http模块效率过低了,就能够换掉他。本身用socket实现一个。http.req.url变成了http.req.uri。koa就能够把本身的request模块中的url=http.req.uri。彻底不影响咱们对koa的使用。
其实这就是用了代理模式,当咱们想扩展其余人的模块时,不如试试。
接下来让咱们举例看下最经常使用的ctx.body吧!函数
set body(val) { const original = this._body; this._body = val; // no content if (null == val) { if (!statuses.empty[this.status]) this.status = 204; this.remove('Content-Type'); this.remove('Content-Length'); this.remove('Transfer-Encoding'); return; } // set the status if (!this._explicitStatus) this.status = 200; // set the content-type only if not yet set const setType = !this.header['content-type']; // string if ('string' == typeof val) { if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'; this.length = Buffer.byteLength(val); return; } // buffer if (Buffer.isBuffer(val)) { if (setType) this.type = 'bin'; this.length = val.length; return; } // stream if ('function' == typeof val.pipe) { onFinish(this.res, destroy.bind(null, val)); ensureErrorHandler(val, err => this.ctx.onerror(err)); // overwriting if (null != original && original != val) this.remove('Content-Length'); if (setType) this.type = 'bin'; return; } // json this.remove('Content-Length'); this.type = 'json'; }
这里用到了es5的语法,set body函数会在为body赋值的时候触发。也就是当咱们为ctx.body赋值时,实际上是调用了上面的方法。代码并不难,就是经过val的值和类型来对_body的值和res的header作一些操做。咱们能够看到里面并无调用res.end(_body)啊。其实生成响应的代码是koa的最后一个中间件。也是koa模块定义的惟一中间件(不过并没在middleWare数组里)。ui
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); } function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; const res = ctx.res; if (!ctx.writable) return; let body = ctx.body; const code = ctx.status; // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; return res.end(); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } return res.end(); } // status body if (null == body) { body = ctx.message || String(code); if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }
handleRequest这个方法最后一句就是执行全部中间件后,再执行respond方法。而respond方法,就是生成最后的响应,按照一些判断条件来调用res.end()
上图中的req对象和res对象,对应着http,request事件中的两个回调参数。app为koa实例。context为中间件中的ctx,request为ctx.request, response为ctx.response。
上图还有一个小问题,那就是app.context,为何ctx不能直接继承context模块,我的认为这个是方便扩展ctx功能的。要为ctx赋值方法首先不能修改context模块。若是要直接修改每个ctx,就要来一次请求,为构造的ctx添加一次方法。有了这个app.context模块,只须要app.context.demo = ()=>{},每一个ctx就有demo方法了。koa-validate这个模块就是经过这种方式扩展ctx的。
本文如有问题,但愿可以指正,不胜感激!