深入koa2源码

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

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

koa 是当下非常流行的 node 框架,相比笨重的 express , koa 只专注于中间件模型的建立,以及请求和响应控制权的转移。本文将以 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 请求中的 host , protocol , ip 分别优先从 Header 字段中的 X-Forwarded-Host , X-Forwarded-Proto , X-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 函数将中间件数组转换成执行链函数 fn , compose 的实现是重点,下文会分析。 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.req 和 ctx.res 可以访问到 node 原生的请求对象和响应对象, 通过修改 ctx.state 可以让中间件共享状态。可以用一张图描述这个函数中定义的关系,如下: 深入koa2源码

接下来我们分析细节, this.context 、 this.request 、 this.response 分别通过 context 、 request 、 response 三个对象的原型创建, 我们先看一下 request 的定义,它位于 request.js 文件中。

request.js

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

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

复制代码

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

属性/方法 含义
header 原生 req 对象的 headers
headers 原生 req 对象的 headers , 同上
url 原生 req 对象的 url
origin protocol://host
href 请求的完整 url
method 原生 req 对象的 method
path 请求 url 的 pathname
query 请求 url 的 query ,对象形式
queryString 请求 url 的 query ,字符串形式
search ?queryString
hostname hostname
URL 完整的URL对象
fresh 判断缓存是否新鲜,只针对 HEAD 和 GET 方法,其余请求方法均返回 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.proxy 是 true 时支持 X-Forwarded-Proto
protocol 返回请求协议, https 或 http 。当 app.proxy 是 true 时支持 X-Forwarded-Proto
host 获取当前主机 (hostname:port) 。当 app.proxy 是 true 时支持 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 的属性多数也定义了 get 和 set 方法。 response 的属性和方法如下:

属性/方法 含义
header 原生 res 对象的 headers
headers 原生 res 对象的 headers , 同上
status 响应状态码, 原生 res 对象的 statusCode
message 响应的状态消息. 默认情况下, response.message 与 response.status 关联
socket 套接字,原生 res 对象的socket
type 获取响应头的 Content-Type 不含参数 charset
body 响应体,支持 string , buffer 、 stream 、 json
lastModified 将 Last-Modified 标头返回为 Date , 如果存在
etag 响应头的 ETag
length 数字返回响应的 Content-Length ,使用 Buffer.byteLength 对 body 进行计算
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 对象的 finished 为 true ,则返回 false , 否则判断原生 res 对象是否建立套接字 socket , 如果没有返回 false , 有则返回 socket.writable

request 和 response 中每个属性 get 和 set 的定义以及方法的实现多数比较简单直观,如果对每个进行单独分析会导致篇幅过长,而且这些不是理解 koa 运行机制的核心所在,因此本文只罗列属性和方法的用途,这些大部分也可以在 koa 的官方文档中找到。关心细节的朋友可以直接阅读 request.js 和 response.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 属性的 set 和 get 方法。 set 方法很简单, COOKIES 是一个 Symbol 类型的私有变量。需要注意的是我们一般不通过 ctx.cookies 来直接设置 cookies ,官方文档推荐使用 ctx.cookies.set(name, value, options) 来设置,可是这里并没有 cookies.set 呀,其实这里稍微一看就明白, cookies 的值是 this[COOKIES] ,它是 Cookies 的一个实例,在 Cookie 这个 npm 包中是定义了实例的 get 和 set 方法的。

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.set 、 this.type 等是哪里来的? context 并没有定义这些属性。我们知道, ctx 中其实是代理了很多 response 和 resquest 的属性和方法的, this.set 、 this.type 其实就是 response.set 和 response.type 。那么 koa 中对象属性和方法的代理是如何实现的呢,答案是 delegate , context 中代码的最后就是使用 delegate 来代理一些本来只存在于 request 和 response上 的属性。接下来我们看一下 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.method 、 Delegator.prototype.accsess 以及 Delegator.prototype.getter ,这些都是代理方法, 分别代理 set 和 get 方法。下面是代码,其中 get 和 set 方法的代理主要使用了对象的 __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;
};
复制代码

到这里,关于 request 、 response 和 context 就聊的差不多了,接下来回到 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 函数,失败后将执行 ctx 的 onerror 函数。 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

respond 是 koa 内置的响应自动处理函数,代码如下,它主要功能是判断 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源码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

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

The Pragmatic Programmer

The Pragmatic Programmer

Andrew Hunt、David Thomas / Addison-Wesley Professional / 1999-10-30 / USD 49.99

本书直击编程陈地,穿过了软件开发中日益增长的规范和技术藩篱,对核心过程进行了审视――即根据需求,创建用户乐于接受的、可工作和易维护的代码。本书包含的内容从个人责任到职业发展,直至保持代码灵活和易于改编重用的架构技术。从本书中将学到防止软件变质、消除复制知识的陷阱、编写灵活、动态和易适应的代码、避免出现相同的设计、用契约、断言和异常对代码进行防护等内容。一起来看看 《The Pragmatic Programmer》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具