# 关于listen做了什么

# listen

  listen() {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }
1
2
3
4
5

我们看到,listen部分的源码很简单,调用了node原生的http模块,然后再调用原生的listen

所以,这里的关键部分是this.callback(),我们来看看这个函数做了什么

# callback

callback() {
    //使用compose函数对中间件处理,***这个函数是koa的重点之一***
    const fn = compose(this.middleware); 
    //监听错误事件
    if (!this.listeners('error').length) this.on('error', this.onerror); 

    const handleRequest = (req, res) => {
      // 默认返回404,如果后面有调用ctx.body之类的方法,会把status改成200
      res.statusCode = 404;
      const ctx = this.createContext(req, res); //创造ctx
      const onerror = err => ctx.onerror(err); //未来用来处理错误的函数
      const handleResponse = () => respond(ctx); //未来用来返回结果的函数
      onFinished(res, onerror); //当res结束时候执行回调
      //执行函数,并将结果交给handleRespons
      return fn(ctx).then(handleResponse).catch(onerror);e
    };

    return handleRequest;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 这一部分有点多,我们分别来看看每个函数的作用

# compose(第3行)

function compose (middleware) {
  // 检查midddleware类型,以及数组每一项是不是函数
  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) {
      //当调用多次next会报错
      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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

看到这部分代码,不知道你是否还记得我们之前说的前置知识点二,这里其实就是一个升级版, 多了一层类型检查Promise处理,这些代码也就是Koa洋葱模型中间件的关键部分啦.

# createcontext(第10行)

  createContext(req, res) {
    //做一系列属性的挂载,使context拥有requset,以及response上面的属性
    const context = Object.create(this.context); //把this.context赋值给context变量
    //先把this.request赋值给context.request变量,然后单拎出来
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    //保存原生的req,res在context里
    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.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里的目的就是创造ctx,这里其实就是做了一系列属性间的相互挂载,为了能够使context拥有requset,以及response上面的属性,并且比较有意思的是保护了原生的req,res不受到污染.

# handleResponse(第15行)

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;

  //...已省略,为了处理一些特殊情况

  // 用res.end响应   ***关键部分***
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // 返回json格式
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这一部分,也就是如果不发生error,函数正常执行的最后一部分,发挥主要作用的其实就是在13-23行的代码,在前面compose的闭包函数执行过后, 对不同的ctx.bodyres.end做不同的处理,返回不同结果,把结果打印到页面上.

至此,一次koa调用,正式结束.