Koa源码阅读心得

987 阅读2分钟

洋葱模型

koa-compose.19dd3bb5

compose函数实现了洋葱模型。主要的思想是递归的调用的dispatch函数dispatch函数在内部是返回一个promise对象,这个promise对象resolve出去一整个‘洋葱’,中间件函数串起来的promise对象。于是,如果在中间件中调用next的时候不使用异步函数async/await处理的话,在内部就会把调用的下一个中间件当作是异步函数处理,就会先去执行后面的代码,导致结果出现问题。

function compose (middleware) {
  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 {
        //fn = (ctx,next) => {ctx.body = "Hello, world" next()}
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
  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);
  }

错误处理

context.onerror

onerror(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;

    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));

    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

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

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return;
    }

    const { res } = this;

    // first unset all headers
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // then set those specified
    this.set(err.headers);

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

    let statusCode = err.status || err.statusCode;

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

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

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

  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

这一块做了两件事:处理了发生error时ctx的情况,和把err emit给application进行处理。

错误处理这一块是调用ctx里的onerror函数,这个函数接受一个err参数,然后通过this.app.emit('error', err, this);把err发射给app里 。当然,这部分是在fnMiddleware里返回的promise对象里的catch完成的

ctx总结

callback() {
    //fn是一个promise包裹的对象
    //fne接受两个参数(context, next)
    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;
  }

这里传给handleRequest里的ctx必须要有node原生的req和res以及自己封装的request和response,这样他才能去里面做逻辑做中间件里的业务。首先看看context,request,response里面做了哪些事

createContext(req, res) {
    const context = Object.create(this.context);
  //把request.js和response.js里的方法导出来放到copntext里,好在context里把ctx代理到response和request上
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.createContextapp = response.app = this;
    //把原生的req和res给到koa封装的request和response对象里
    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;
  }

通过context.js里可以看出来,导出了一些delegate的方法。、导出去了一个proto,这个proto就是createContext里的context。proto主要是下面两个代理,代理了response和request。这样一来,我们对上下文 ctx 的操作,如 ctx.typectx.length 就会由 response 对象执行,ctx.pathctx.method 就会由 request 对象执行。 responserequest 是 Koa 自己的请求和响应。

//context.js
const delegate = ("delegates");
​
module.exports = {
  inspect() {},
  toJSON() {},
  throw() {},
  onerror() {},
};
​
const proto = module.exports;
​
delegate(proto, "response")
  .method("set")
  .method("append")
  .access("message")
  .access("body");
​
delegate(proto, "request")
  .method("acceptsLanguages")
  .method("accepts")
  .access("querystring")
  .access("socket");
​
​

真正将请求与响应的操作落实到位的不是上下文 ctx ,而是来自 request.jsrequest 对象和来自 response.jsresponse 对象。我们看一下这两个对象的实现。

/* request.js */module.exports = {
  /* ... */
  
  /**
   * Get request URL.
   *
   * @return {String}
   * @api public
   */
​
  get url() {
    return this.req.url;
  },
  
  /* ... */
}
/* response.js */module.exports = {
  /* ... */
  
  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */
​
  get headerSent() {
    return this.res.headersSent;
  },
  
  /* ... */
}

是靠 Koa 请求/响应去操作 Node 请求/响应来实现的!整个流程串起来就是,上下文 ctx 委托给 Koa 请求/响应,Koa 请求/响应操作 Node 请求/响应,从而实现了完整的请求/响应处理流程。