深入koa2源码

深入koa2源码

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

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

构造函数

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

 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个属性,各自的含义如下:

属性 含义
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便于链式调用。

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函数的返回值。

  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方法去处理回调逻辑。

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可以让中间件共享状态。可以用一张图描述这个函数中定义的关系,如下:

接下来我们分析细节,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);
}
复制代码
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/101218.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • ArcGIS二次开发基础教程(07):简单符号及图层渲染「建议收藏」

    ArcGIS二次开发基础教程(07):简单符号及图层渲染「建议收藏」ArcGIS二次开发基础教程(07):简单符号及图层渲染简单渲染0.点渲染IGeoFeatureLayerGetLayerByName(stringname){ILayerlayer=null;for(inti=0;i<axMapConTrol1.LayerCount;i++){layer=axMapControl1….

    2022年7月23日
    15
  • Composer常见问题

    Composer常见问题

    2021年11月7日
    41
  • tinyxml动态库生成和使用「建议收藏」

    tinyxml动态库生成和使用「建议收藏」1、xml默认是生成执行文件,要想生成动态库需要修改makefile从http://ncu.dl.sourceforge.net/sourceforge/tinyxml/tinyxml_2_4_0.tar.gz下载tinyxml,可以根据自己的需要,选择不同的版本。将tinyxml_2_4_0.tar.gz上传到主机,然后解压执行如下命令: tar-xzvftinyxml_2_

    2022年6月7日
    36
  • php建站错误代码0xc0000005,0xc0000005是什么错误-0xc0000005错误代码解决方法介绍-沧浪系统…

    php建站错误代码0xc0000005,0xc0000005是什么错误-0xc0000005错误代码解决方法介绍-沧浪系统…我们的电脑在进行各种各样的操作的时候有时会出现一些我们看不懂的报错,这种不知道错在哪了的问题让人很烦恼,下面就让小编来给大家介绍一下0xc0000005错误代码解决方法介绍吧。0xc0000005错误代码介绍0xc0000005这个错误一般是以显卡驱动模块相关,或者是第三方软件而引起的系统错误。解决方法一、如果是问题出在系统模块,那就需要厂商网站下载适用系统的最新驱动。二、如果是第三方软件引起的就…

    2022年9月1日
    2
  • ubuntu中利用ffmeg将短视频转化为动图.gif.「建议收藏」

    ubuntu中利用ffmeg将短视频转化为动图.gif.「建议收藏」命令:ffmpeg-ss00:00:1-i/home/jiangcm/Pictures/session_gpus_pre.mp4-to00:00:8-r4-vfscale=700:-1/home/jiangcm/Pictures/session_gpus_pre.gif解读:-ss00:00:1:表示从第1秒开始;-i:表示invert,转换,后面跟着转换的视频;-to:表示结束时间;-r4:4帧率;vfscale=700:-1:制定宽度,高度为原来的倍;/ho

    2022年9月14日
    4
  • PLD,CPLD,FPGA区别[通俗易懂]

    PLD,CPLD,FPGA区别[通俗易懂]入门以后可以学习Xilinx的ISE,Altera的QuartusII学习CPLD初学者,建议选用LATTICE,这家公司在此方面有优势主流还是Altera和Xilinx,毕竟是最大的两家PLD公司(Cyclone   Spartan) PLD,CPLD,FPGA有何不同?不同厂家的叫法不尽相同,  PLD(ProgrammableLogicDevice)是可编程逻辑器件的总称

    2022年5月4日
    95

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号