Koa2洋葱模型

浅解析洋葱模型

koa2最大的特点就是独特的中间件控制流程。koa2基于NodeV7.6.0,可以直接使用async/await来替代generator,使得代码彻底摆脱“回调地狱”,可读性更佳。

洋葱模型

以下是一张中间件的“神图”,它可以清晰的表示出:从代码请求到响应的过程中,中间件的工作流程。
image.png

小demo

先看个小🌰吧:

const Koa = require('koa');

const app = new Koa();

app.use(async(ctx, next) => {
    console.log(1)
    await next();
    console.log(2)
});

app.use(async(ctx, next) => {
    console.log(3)
    await next();
    console.log(4)
});

app.use(async(ctx, next) => {
    console.log(5)
});

app.listen(3000, () => {
    console.log('koa服务器已启动')
});

app.on('error', (err) => {
    console.log(err)
});
//1 3 5 4 2

启动demo,访问服务器地址,控制台就会输出答案: 1、3、5、4、2 。
从这个🌰中,我们可以看出,当程序执行遇到await next() 的时候,会暂停执行当前代码块,进入下一个中间件,等下一个中间件执行完之后再回过头来继续处理。

小结

当在一个中间件中调用await next()时,当前中间件就会把程序的控制权交给下一个中间件。当下游没有中间件等待执行的时候,程序就会回溯上游,执行上游中间件等待执行的行为。

源码解析

koa2的源码只有四个文件,详情请查看Koa2源码

  • application.js:入口文件。
  • context.js:应用上下文文件。
  • request.js:请求处理文件。
  • response.js:响应处理文件。

先从入口文件看起:

application.js

/**
     * Shorthand for:
     *
     *    http.createServer(app.callback()).listen(...)
     *
     * @param {Mixed} ...
     * @return {Server}
     * @api public
     */

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

在入口文件中,我们可以看到,其实我们经常使用的app.listen(port),只是一个“语法糖”,它只是使用来 this.callback() 来生成nodehttpServer的回调函数。我们来看this.callback()

/**
     * Return a request handler callback
     * for node's native http server.
     *
     * @return {Function}
     * @api public
     */

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;
}

在这段代码中,首先使用compose将中间件组合起来,compose就是一个柯里化函数,它接受中间件数组,返回一个函数,在返回函数中,接受应用上下文ctx和next,返回一个promise对象。
compose源码

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  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)
      }
    }
  }
}

compose函数中,最重要的就是使用diapatch()函数,遍历中间件数组,然后将ctx和dispatch(i + 1)传递给中间件的方法中。它做到了以下两点:

  • 将ctx应用文对象一直传递下去。
  • 将下一个中间件函数作为未来的next的返回值。

this.callback()中也使用了createContext()handleRequest()方法,我们来看下它们做来什么。

/**
     * Handle request in callback.
     *
     * @api private
     */

    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);
    }

    /**
     * Initialize a new context.
     *
     * @api private
     */

    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()中,返回一个应用上下文对象context。在handleRequest()中,将应用上下文ctx和中间件进行绑定。
在Koa2中,我们使用中间件的时候需要使用app.use()来注册中间件,那么app.use()到底做来什么呢?

 /**
     * Use the given middleware `fn`.
     *
     * Old-style middleware will be converted.
     *
     * @param {Function} fn
     * @return {Application} self
     * @api public
     */

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;
}

去除注释和验证,它只做了两件事:

  • 将中间件函数push到中间件数组(栈)。
  • 返回koa实例对象。
    use(fn) {
    this.middleware.push(fn);
    return this;
    }

总结

所谓“洋葱模型”,就是当执行中间件函数的时候,如果碰到await next(),就会执行下一个中间件(上面说过,中间件注册的时候,会把下一个中间件当作未来的next()函数),这里next()就是下一个中间件函数。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!