使用過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的。
本文如有問題,但願可以指正,不勝感激!