深入koa2源码

栏目: Node.js · 发布时间: 5年前

内容简介:根据首先看一下构造函数的代码

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

深入koa2源码

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

构造函数

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

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

接下来我们分析细节, 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 , TRACE 6个方法
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);
}
复制代码

以上所述就是小编给大家介绍的《深入koa2源码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python带我起飞

Python带我起飞

李金洪 / 电子工业出版社 / 2018-6 / 79

《Python带我起飞——入门、进阶、商业实战》针对Python 3.5 以上版本,采用“理论+实践”的形式编写,通过大量的实例(共42 个),全面而深入地讲解“Python 基础语法”和“Python 项目应用”两方面内容。书中的实例具有很强的实用性,如对医疗影像数据进行分析、制作爬虫获取股票信息、自动化实例、从一组看似混乱的数据中找出规律、制作人脸识别系统等。 《Python带我起飞——......一起来看看 《Python带我起飞》 这本书的介绍吧!

html转js在线工具
html转js在线工具

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具