kobject架构 koa框架原理_Koa

基础结构和骨架

首先,koa 是一个 node 框架:

  • 基于 node 原生 req 和 res,并基于封装的自定义的request和response对象封装成一个统一的context 对象。
  • 基于 async/await(generator)的洋葱模型实现了中间件机制。

kobject架构 koa框架原理_Koa_02

其目录结构和负责的功能如下:

── lib
   ├── application.js 
   ├── context.js
   ├── request.js
   └── response.js

1. application.jskoa 的主入口,是核心部分,完成 koa 实例初始化的工作,启动服务器:

kobject架构 koa框架原理_封装_03

2. context.js

kobject架构 koa框架原理_中间件_04

3. request.js

kobject架构 koa框架原理_Koa_05

4. response.js 

kobject架构 koa框架原理_中间件_06

工作流

分为三步:

        1. 初始化。new 初始化一个实例,包括创建中间件数组、创建 context/request/response 对象,再使用 use(fn) 添加中间件到 middleware 数组,最后使用 listen 合成中间件 fnMiddleware,按照洋葱模型依次执行中间件,返回一个 callback 函数给 http.createServer,开启服务器,等待 http 请求。

kobject架构 koa框架原理_kobject架构_07

        2. 请求。每次请求,createContext 生成一个新的 ctx,传给 fnMiddleware,触发中间件的整个流程。

        3. 响应。整个中间件完成后,调用 respond 方法,对请求做最后的处理,返回响应给客户端。

中间件机制与实现

中间件机制是采用 koa-compose 实现的,compose 函数接收 middleware 数组作为参数, middleware 中每个对象都是 async 函数,返回一个以 context 和 next 作为入参的函数,称其为 fnMiddleware。在外部调用 this.handleRequest 的最后一行,运行了中间件:

fnMiddleware(ctx).then(handleResponse).catch(onerror);

在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。 

中间件所构成的执行栈如下图所示,

kobject架构 koa框架原理_javascript_08

其中 next就是一个含有 dispatch方法的函数。

在第 n 个中间件执行 next时,相当于在执行 dispatch(n + 1),就进入到第 n + 1 个中间件的处理流程。

因为 dispatch返回的都是 Promise对象,因此在第 n 个中间件 await next()时,就进入到了第 n+1 个中间件,而当第 n+1 个中间件执行完成后,可以返回第n个中间件。

如果在某个中间件中没有调用 next(),就不会再执行它后面所有的中间件。

在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session 处理等等。其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机

比如,我们需要知道一个请求或者操作 db 的耗时是多少,而且想获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到:

/** 外层洋葱 */
app.use(async(ctx, next) => {
  const start = new Date();
  await next();
  const delta = new Date() - start;
  console.log (`请求耗时: ${delta} MS`);
  console.log('拿到上一次请求的结果:', ctx.state.baiduHTML);
})

/** 内层洋葱 */
app.use(async(ctx, next) => {
  // 处理 db 或者进行 HTTP 请求
  ctx.state.baiduHTML = await axios.get('http://baidu.com');
})

这就相当于是在 内层中间件 -- “处理 db 或者进行 HTTP 请求” 上进行 AOP 切面编程扩展打印请求耗时的功能。 而假如没有洋葱模型,这是做不到的。

koa-convert

koa2 中引入了 koa-convert 库,在使用use函数时,会使用到convert方法:

const convert = require('koa-convert');

module.exports = class Application extends Emitter {
    use(fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
        if (isGeneratorFunction(fn)) {
            deprecate('Support for generators will be removed');
            fn = convert(fn);
        }
        debug('use %s', fn._name || fn.name || '-');
        this.middleware.push(fn);
        return this;
    }
}

koa2 框架针对 koa1 版本作了兼容处理,中间件函数如果是 generator函数,会被 koa-convert转换为“类async函数”。generator函数和 async函数的区别是 async函数会自动执行,而 generator每次都要调用 next 函数才能执行,因此我们需要寻找到一个合适的方法,让 next()函数能够一直持续下去即可,这时可以将 generator中 yield的 value指定成一个 Promise对象。下面看看koa-convert中的核心代码:

const co = require('co')
const compose = require('koa-compose')

module.exports = convert

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    return mw
  }
  const converted = function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

首先针对传入的参数 mw 作校验,如果不是函数则抛异常,如果不是 generator函数则直接返回,如果是 generator函数则使用 co函数进行处理。co 的核心代码如下:

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);
  
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();
    
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
      return null;
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

可以看出,处理如下:

  • 把一个 generator封装在一个Promise对象中;
  • 这个 Promise对象再次把它的 gen.next()也封装出Promise对象,相当于这个子 Promise对象完成的时候也重复调用 gen.next();
  • 当所有迭代完成时,对父 Promise对象进行 resolve。

异步函数的统一错误处理机制

在koa框架中,有两种错误的处理机制——中间件捕获框架捕获。

中间件捕获是针对中间件做了错误处理响应,如在中间件运行出错时,会触发 onerror 监听函数:

fnMiddleware(ctx).then(handleResponse).catch(onerror)

框架捕获是在 context.js中作了相应的处理:

/** this.app是对 application的引用 */
this.app.emit('error', err, this)

当 context.js调用 onerror时,实际上是触发application实例的 error事件 ,因为 Application类是继承自 EventEmitter类的,因此具备了处理异步事件的能力,可以使用 EventEmitter类中对于异步函数的错误处理方法。

因为 async函数返回的是一个 Promise对象,如果 async函数内部抛出了异常,则会导致Promise对象变为 reject状态,异常会被 catch的回调函数(onerror)捕获到。而且,如果 await 后面的 Promise 对象变为reject状态,reject 的参数也可以被catch的回调函数(onerror)捕获到。

委托模式

委托模式,即外层暴露的对象将请求委托给内部的其他对象进行处理。

delegates 基本用法就是将内部对象的变量或者函数绑定在暴露在外层的变量上,直接通过 delegates 方法进行如下基本的委托:

  • getter:外部对象可以直接访问内部对象的值;
  • setter:外部对象可以直接修改内部对象的值;
  • access:包含 getter 与 setter 的功能;
  • method:外部对象可以直接调用内部对象的函数。

delegates 原理就是 __defineGetter__和 __defineSetter__。在 application.createContext 函数中,被创建的 context 对象会挂载基于 request.js实现的 request对象和基于 response.js 实现的 response 对象。delegate的作用是让context对象代理request和response的部分属性和方法。

因此通过 this.ctx.xx取到 this.ctx.request或 this.ctx.response下挂载的 xx方法。

在 context.js中,只需要代理属性即可,delegate方法只代理属性,足够满足此效果,而在response.js和request.js中是需要处理其他逻辑的,所以需要使用 set 和 get 方法实现来处理一些额外的逻辑处理,比如 query 进行格式化操作:

get query() {
  const str = this.querystring;
  const c = this._querycache = this._querycache || {};
  return c[str] || (c[str] = qs.parse(str));
}