Time Bomb Veela
  1. 1 Time Bomb Veela
  2. 2 Libertus Chen-U
  3. 3 Warcry mpi
  4. 4 Quiet Storm Lyn
  5. 5 One Last You Jen Bird
  6. 6 Life Will Change Lyn
  7. 7 Flower Of Life 发热巫女
  8. 8 The Night We Stood Lyn
  9. 9 かかってこいよ NakamuraEmi
  10. 10 Last Surprise Lyn
  11. 11 Hypocrite Nush
2017-11-24 12:26:56

Koa2原理解析

Koa是继Express之后的又一大方异彩的后端框架。相比于Express其最大的变化在于支持async/await的异步流程控制以及中间件的变化。本文将按照流程对其原理进行讨论。


启动前

在Koa程序运行前存在着三个静态方法集合如下:

Request

包含着操作Node原生请求对象的一些方法,例如获取query,请求url等。

Response

包含着操作Node原生响应对象的一些方法,例如设置状态码,header等。

Context

这是Koa最重要的概念之一,如其名是上下文的意思,Request和Response自身的方法都会委托到Context中。

Context又分为两个部分,一部分是自身属性,一部分是Request和Response委托的操作方法,主要是为了给用户提供更方法获取参数的方法。源码如下:

var delegate = require('delegates');
var proto = module.exports = {}; // 一些自身方法,被我删了

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

method是委托方法,getter委托getter,access委托getter和setter。


启动Server

Koa的启动很简单,就分为两步。第一步new一个Koa实例,第二步调用listen方法监听端口即可。看起来好像是在第一步的时候启动了服务器,实际上并不是。先看启动部分的源码:

module.exports = class Application extends Emitter {

  /**
   * Initialize a new `Application`.
   *
   * @api public
   */

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

可以看到第一步并没有启动服务器,但还是初始化了一些很有用的东西。包括初始化了一个中间件集合数组,以及Request,Response,Context的创建和委托,继承Emitter。

接下来看listen方法的源码:

app.listen = function(){
  debug('listen');
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};

app.callback = function() {
  const fn = compose(this.middleware);

  if (!this.listeners('error').length) this.on('error', this.onerror);

  return (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
  };
}

可以看到在调用监听方法时启动了一个server并立即执行了一个callback函数。这个函数内部可以分成两部分看,第一部分是执行函数时的代码,也就是初始化中间件。另一部分是接受请求后所执行的代码。


初始化中间件

通过compose函数去处理中间件数组,compose的全名叫koa-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!')
  }

  /**
   * @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
      const fn = middleware[i] || next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

首先会执行dispatch函数并传入参数0,然后会递归调用参数并依次加1,该参数对应着每次取到的中间件。取到中间件后会执行并传递context和next两个参数。

Context则是koa的上下文,next函数则是返回下一个dispatch函数的执行结果,也就是会去执行下一个中间件。可以看到每一个中间件执行结果都被一个Promise包装,并且如果需要决议掉该Promise,需要等到下一个中间件Promise决议完成。而在下一个中间件中一旦调用next又会触发新一轮的中间件连锁,以此类推,则实现了洋葱式结构。

该compose函数会返回dispatch(0),则是第一个中间件函数准备启动的状态了。下面就该接受请求并启动了。


接受请求

首先通过createContext创建一个完整版的ctx,这不仅包括了原版context的自有属性和委托属性,还挂载了原生req对象,res对象和app对象。源码如下:

app.createContext = function(req, res){

  // 继承
  var context = Object.create(this.context);
  var request = context.request = Object.create(this.request);
  var response = context.response = Object.create(this.response);

  // 往context,request,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.onerror = context.onerror.bind(context);
  context.originalUrl = request.originalUrl = req.url;
  context.cookies = new Cookies(req, res, {
    keys: this.keys,
    secure: request.secure
  });
  context.accept = request.accept = accepts(req);
  context.state = {};

  // 最后返回完整版context
  return context;
};

然后通过onFinished函数监听response,如果发生错误就会执行ctx.onerror,用于设置response的错误信息。

onerror: function(err){
  // don't do anything if there is no error.
  // this allows you to pass `this.onerror`
  // to node-style callbacks.
  if (null == err) return;

  if (!(err instanceof Error)) err = new Error('non-error thrown: ' + err);

  // delegate
  this.app.emit('error', err, this);

  // nothing we can do here other
  // than delegate to the app-level
  // handler and log.
  if (this.headerSent || !this.writable) {
    err.headerSent = true;
    return;
  }

  // unset all headers
  this.res._headers = {};

  // force text/plain
  this.type = 'text';

  // ENOENT support
  if ('ENOENT' == err.code) err.status = 404;

  // default to 500
  if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;

  // respond
  var code = statuses[err.status];
  var msg = err.expose ? err.message : code;
  this.status = err.status;
  this.length = Buffer.byteLength(msg);
  this.res.end(msg);
}

最后则是启动第一个中间件触发整个流程了,最后通过respond函数对最终的ctx做处理,来决定以什么类型响应请求。

function respond() {
  // allow bypassing koa
  if (false === this.respond) return;

  var res = this.res;
  if (res.headersSent || !this.writable) return;

  var body = this.body;
  var code = this.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    this.body = null;
    return res.end();
  }

  if ('HEAD' == this.method) {
    if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
    return res.end();
  }

  // status body
  if (null == body) {
    this.type = 'text';
    body = this.message || String(code);
    this.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);
  this.length = Buffer.byteLength(body);
  res.end(body);
}

总结

整套流程大概分为3部分:

请求前——初始化中间件

使用use函数将所有中间件函数收集到middleware数组,在监听时利用compose函数将其转化为洋葱式结构

请求时——创建上下文

利用createContext函数生成最终上下文并启动第一个中间件函数触发整个流程。

请求后——处理最终上下文

利用respond函数对最终返回到客户端的请求做处理。

可以看到,代码虽少,但是非常优雅,值得仔细的学习。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「Node.js」 标签。