koa源碼分析(二)ctx

爲何須要ctx

使用過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的定義

要找到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模塊以前,讓咱們先回顧下,context模塊和咱們日常寫中間件用的ctx之間的關係。 context
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

respose模塊和reqeust模塊

這連個模塊的功能分別就是封裝了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()

總結

ctx結構
上圖中的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的。
本文如有問題,但願可以指正,不勝感激!

相關文章
相關標籤/搜索