深刻koa2源码

koa是当下很是流行的node框架,相比笨重的expresskoa只专一于中间件模型的创建,以及请求和响应控制权的转移。本文将以koa2为例,深刻源码分析框架的实现细节。 koa2的源码位于lib目录,结构很是简单和清晰,只有四个文件,以下:javascript

根据package.json中的main字段,能够知道入口文件是lib/application.js,application.js定义了koa的构造函数以及实例拥有的方法,以下图: java

构造函数

首先看一下构造函数的代码node

constructor() {
    super();
    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
复制代码

这里定义了实例的8个属性,各自的含义以下:git

属性 含义
proxy 表示是否开启代理,默认为false,若是开启代理,对于获取request请求中的hostprotocolip分别优先从Header字段中的X-Forwarded-HostX-Forwarded-ProtoX-Forwarded-For获取。
middleware 最重要的一个属性,存放全部的中间件,存放和执行的过程后文细说。
subdomainOffset 子域名的偏移量,默认值为2,这个参数决定了request.subdomains的返回结果。
env node的执行环境, 默认是development
context 中间件第一个实参ctx的原型, 具体在讲context.js时会说到。
request ctx.request的原型,定义在request.js中。
response ctx.response的原型,定义在response.js中。
[util.inspect.custom] util.inspect这个方法用于将对象转换为字符串, 在node v6.6.0及以上版本中util.inspect.custom是一个Symbol类型的值,经过定义对象的[util.inspect.custom]属性为一个函数,能够覆盖util.inspect的默认行为。

use()

use方法很简单,接受一个函数做为参数,并加入middleware数组。因为koa最开始支持使用generator函数做为中间件使用,但将在3.x的版本中放弃这项支持,所以koa2中对于使用generator函数做为中间件的行为给与将来将被废弃的警告,但会将generator函数转化为async函数。返回this便于链式调用。github

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
复制代码

listen()

下面是listen方法,能够看到内部是经过原生的http模块建立服务器并监听的,请求的回调函数是callback函数的返回值。express

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
复制代码

callback()

下面是callback的代码,compose函数将中间件数组转换成执行链函数fncompose的实现是重点,下文会分析。koa继承自Emitter,所以能够经过listenerCount属性判断监听了多少个error事件, 若是外部没有进行监听,框架将自动监听一个error事件。callback函数返回一个handleRequest函数,所以真正的请求处理回调函数是handleRequest。在handleRequest函数内部,经过createContext建立了上下文ctx,并交给koa实例的handleRequest方法去处理回调逻辑。npm

callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return 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.state = {};
    return context;
}
复制代码

上面是createContext的代码, 从这里咱们能够知道,经过ctx.reqctx.res能够访问到node原生的请求对象和响应对象, 经过修改ctx.state可让中间件共享状态。能够用一张图描述这个函数中定义的关系,以下: json

接下来咱们分析细节,this.contextthis.requestthis.response分别经过contextrequestresponse三个对象的原型建立, 咱们先看一下request的定义,它位于request.js文件中。数组

request.js

request.js定义了ctx.request的原型对象的原型对象,所以该对象的任意属性均可以经过ctx.request获取。这个对象一共有20多个属性和若干方法。其中属性多数都定义了getset方法,截取一小部分代码以下:缓存

module.exports = {
 get header() {
    return this.req.headers;
 },
 set header(val) {
    this.req.headers = val;
 },
 ...
}

复制代码

上面代码中定义了header属性,根据前面的关系图可知,this.req指向的是原生的req,所以ctx.request.header等于原生reqheaders属性,修改ctx.request.header就是修改reqheadersrequest对象中全部的属性和方法列举以下:

属性/方法 含义
header 原生req对象的headers
headers 原生req对象的headers, 同上
url 原生req对象的url
origin protocol://host
href 请求的完整url
method 原生req对象的method
path 请求urlpathname
query 请求urlquery,对象形式
queryString 请求urlquery,字符串形式
search ?queryString
hostname hostname
URL 完整的URL对象
fresh 判断缓存是否新鲜,只针对HEADGET方法,其他请求方法均返回false
stale fresh取反
idempotent 检查请求是否幂等,符合幂等性的请求有GET, HEAD, PUT, DELETE, OPTIONS, TRACE6个方法
socket 原生req对象的套接字
charset 请求字符集
type 获取请求头的Content-Type 不含参数 charset
length 请求的 Content-Length
secure 判断是否是https请求
ips X-Forwarded-For 存在而且 app.proxy 被启用时,这些 ips的数组被返回,从上游到下游排序。 禁用时返回一个空数组。
ip 请求远程地址。 当 app.proxytrue 时支持 X-Forwarded-Proto
protocol 返回请求协议,httpshttp。当 app.proxytrue 时支持 X-Forwarded-Proto
host 获取当前主机(hostname:port)。当 app.proxytrue 时支持 X-Forwarded-Host,不然使用Host
subdomains 根据app.subdomainOffset设置的偏移量,将子域返回为数组
get(...args) 获取请求头字段
accepts(...args) 检查给定的 type(s) 是否能够接受,若是 true,返回最佳匹配,不然为 false
acceptsEncodings(...args) 检查 encodings 是否能够接受,返回最佳匹配为 true,不然为 false
acceptsCharsets(...args) 检查 charsets 是否能够接受,在 true 时返回最佳匹配,不然为 false
acceptsLanguages(...args) 检查 langs 是否能够接受,若是为 true,返回最佳匹配,不然为 false
[util.inspect.custom] 自定义的util.inspect

response.js

response.js定义了ctx.response的原型对象的原型对象,所以该对象的任意属性均可以经过ctx.response获取。和request相似,response的属性多数也定义了getset方法。response的属性和方法以下:

属性/方法 含义
header 原生res对象的headers
headers 原生res对象的headers, 同上
status 响应状态码, 原生res对象的statusCode
message 响应的状态消息. 默认状况下, response.messageresponse.status 关联
socket 套接字,原生res对象的socket
type 获取响应头的 Content-Type 不含参数 charset
body 响应体,支持stringbufferstreamjson
lastModified Last-Modified 标头返回为 Date, 若是存在
etag 响应头的ETag
length 数字返回响应的 Content-Length,使用Buffer.byteLengthbody进行计算
headerSent 检查是否已经发送了一个响应头, 用于查看客户端是否可能会收到错误通知
vary(field) field 上变化。
redirect(url, alt) 执行重定向
attachment(filename, options) Content-Disposition 设置为 “附件” 以指示客户端提示下载。(可选)指定下载的 filename
get(field) 返回指定的响应头部
set(field, val) 设置响应头部
is(type) 响应类型是不是所提供的类型之一
append(field, val) 设置规范以外的响应头
remove(field) 删除指定的响应头
flushHeaders() 刷新全部响应头
writable() 判断响应是否可写,原生res对象的finishedtrue,则返回false, 不然判断原生res对象是否创建套接字socket, 若是没有返回false, 有则返回socket.writable

requestresponse中每一个属性getset的定义以及方法的实现多数比较简单直观,若是对每一个进行单独分析会致使篇幅过长,并且这些不是理解koa运行机制的核心所在,所以本文只罗列属性和方法的用途,这些大部分也能够在koa的官方文档中找到。关心细节的朋友能够直接阅读request.jsresponse.js这两个文件,若是你熟悉http协议,相信这些代码对你并无障碍。接下来咱们的重点是context.js

context.js

context.js定义了ctx的原型对象的原型对象, 所以这个对象中全部属性均可以经过ctx访问到。context.js中除了定义[util.inspect.custom]这个不是很重要的属性外,只直接定义了一个属性cookies,也定义了几个方法,这里分别进行介绍:

cookies
get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
复制代码

上面的代码中定义了cookies属性的setget方法。set方法很简单,COOKIES是一个Symbol类型的私有变量。须要注意的是咱们通常不经过ctx.cookies来直接设置cookies,官方文档推荐使用ctx.cookies.set(name, value, options)来设置,但是这里并无cookies.set呀,其实这里稍微一看就明白,cookies的值是this[COOKIES],它是Cookies的一个实例,在Cookie这个npm包中是定义了实例的getset方法的。

throw()
throw(...args) {
    throw createError(...args);
  },
复制代码

当咱们调用ctx.throw抛出一个错误时,内部是抛出了一个有状态码和信息的错误,createError的实如今http-errors这个npm包中。

onerror()

下面是onerror方法的代码,发生错误时首先会触发koa实例上的error事件来打印一个错误日志, headerSent变量表示响应头是否发送,若是响应头已经发送,或者响应处于不可写状态,将没法在响应中添加错误信息,直接退出该函数,不然须要将以前写入的响应头部信息清空。

onerror(err) {
    // 没有错误时什么也不作
    if (null == err) return;
    // err不是Error实例时,使用err建立一个Error实例
    if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));

    let headerSent = false;
    // 若是res不可写或者请求头已发出
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    // 触发koa实例app的error事件
    this.app.emit('error', err, this);

    if (headerSent) {
      return;
    }

    const { res } = this;

    // 移除全部设置过的响应头
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // 设置错误头部
    this.set(err.headers);

    // 设置错误时的Content-Type
    this.type = 'text';

    // 找不到文件错误码设为404
    if ('ENOENT' == err.code) err.status = 404;

    // 不能被识别的错误将错误码设为500
    if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;

    const code = statuses[err.status];
    const msg = err.expose ? err.message : code;
    // 设置错误码
    this.status = err.status;
    this.length = Buffer.byteLength(msg);
    // 结束响应
    res.end(msg);
  },
复制代码

从上面代码中会有疑问, this.setthis.type等是哪里来的?context并无定义这些属性。咱们知道, ctx中实际上是代理了不少responseresquest的属性和方法的,this.setthis.type其实就是response.setresponse.type。那么koa中对象属性和方法的代理是如何实现的呢,答案是delegate,context中代码的最后就是使用delegate来代理一些原本只存在于requestresponse上的属性。接下来咱们看一下delegete是如何实现代理的,delegete的实现代码在delegetes这个npm包中。

delegate

delegate方法本质上是一个构造函数,接受两个参数,第一个参数是代理对象,第二个参数是被代理的对象,下面是它的定义, Delegator就是delegate。能够看到,不论是否使用new关键字,该函数老是会返回一个实例。

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构造函数的原型上,定义了几个方法,koa中用到了Delegator.prototype.methodDelegator.prototype.accsess以及Delegator.prototype.getter,这些都是代理方法, 分别代理setget方法。下面是代码,其中getset方法的代理主要使用了对象的__defineGetter__以及__defineSetter__方法。

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.prototype.access = function(name){
  return this.getter(name).setter(name);
};
Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);

  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};
Delegator.prototype.setter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.setters.push(name);

  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });

  return this;
};
复制代码

到这里,关于requestresponsecontext就聊的差很少了,接下来回到callback继续咱们的重点,前面说到的compose才是koa的精华和核心所在,他的代码在koa-compose这个包中,咱们来看一下:

compose

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码

函数接收一个middleware数组为参数,返回一个函数,给函数传入ctx时第一个中间件将自动执行,之后的中间件只有在手动调用next,即dispatch时才会执行。另外从代码中能够看出,中间件的执行是异步的,而且中间件执行完毕后返回的是一个Promise,每一个dispatch的返回值也是一个Promise,所以咱们的中间件中能够方便地使用async函数进行定义,内部使用await next()调用“下游”,而后控制流回“上游”,这是更准确也更友好的中间件模型。从下面的代码能够看到,中间件顺利执行完毕后将执行respond函数,失败后将执行ctxonerror函数。onFinished(res, onerror)这段代码是对响应处理过程当中的错误监听,即handleResponse发生的错误或自定义的响应处理中发生的错误。

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);
  }
复制代码

respond

respondkoa内置的响应自动处理函数,代码以下,它主要功能是判断ctx.body的类型,而后自动完成最后的响应。另外,若是在koa中须要自行处理响应,能够设置ctx.respond = false,这样内置的respond就会被忽略。

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) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      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);
}
复制代码
相关文章
相关标签/搜索