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模块的新代码。

ctx的定义

要找到ctx定义的地方,最直接的方式就是从http.createServer(callback)里的callback中找。上一节,我们看到koa脚手架,传入的callback就是app.callback()。而app是new了koa。所以我们找到koa中的callback方法。

  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分别继承了引入的三个模块。下面我们来看下这三个模块。

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个方法,还不是我们常用的。其实后面还有两句,其中一句是:

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方法。

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。都是经过了两次继承。

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数组里)。

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的。
本文若有问题,希望能够指正,不胜感激!

转载于:https://my.oschina.net/u/3361610/blog/1649987

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值