koa 源码分析

211 阅读3分钟

koa 源码分析

Koa===Application ,创建一个application实例

  constructor(options) {
    super(); // Application 继承了events对象。
    /** ... */
    this.middleware = [];  // 用来存放中间件
    this.context = Object.create(context); // 引入context对象
    this.request = Object.create(request); // 引入koa 的request对象
    this.response = Object.create(response); // 引入koa的response对象
  }

koa 核心代码

Koa 是个web 框架,它核心是提供一个web服务,入口函数我们是app.listen()

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

listen代码很简单,只有三行;本质上还是利用了原生的http package 启动了一个http server,如果不用koa框架,我们用原生的写法如下:

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html');
  res.setHeader('X-Foo', 'bar');
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('ok');
});

所以看到这里我们就能猜到this.callback(),应该返回一个(req,res)=>{}这样的一个回调函数。

  1. callback()
  callback() {
    const fn = compose(this.middleware); // 中间件的处理,这里可以调到下一章看中间件机制,这里返回一个(ctx,next)=>{}这样的函数。

    if (!this.listenerCount('error')) this.on('error', this.onerror); // 如果没有添加错误的监听回调,这里会添加一个默认的。

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res); // 创建一个ctx对象
      return this.handleRequest(ctx, fn);  // 处理请求和返回响应
    };

    return handleRequest; // 这里返回一个函数,和我们在listen中的猜想是一样的。
  }
  1. handleRequest()
  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);
  }

我们在用Koa框架的时候,我们把我们想要的返回值,赋值给ctx.body:

ctx.body='hello world'; // 返回一个字符串

ctx.body={hello:'world'}; // 返回一个json对象

这个部分的处理是在handleResponse处理的,handleResponse调用了respond();

function (){
   //...
  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);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

中间件的原理

koa源码的另外一个亮点就是它的中间件机制,我们可以使用use()添加一个中间件:

use(fn){
   /*
   ...判断 是否是generator函数,并做出warn 并做转换
   */
    this.middleware.push(fn); // 核心代码只有这一行,middleware= [];
    return this;
}

洋葱模型

中间件的原理是基于洋葱模型的,什么是洋葱模型,这里盗了一张图,来解释一下。

koa主要利用了async和await的特性,执行到下一个中间件的地方暂停执行,执行完后再回复继续执行。

中间件的使用

我们从上面可以看到use只是把中间件保存下来,然后在callback()函数里面用到了

const fn = compose(this.middleware);

compose 引入的一个第三方package koa-compose, 我们看一个这个包的关键代码


function compose(middleware) {
  // 返回了一个和中间件声明一样的函数
  return function (context, next) {
    // last called middleware #
    let index = -1  // 记录已经执行到中间件的索引
    return dispatch(0) // 调用dispatch(0) 从第一个中间件开始执行
    // 声明了一个内部函数dispatch
    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)));// 中间件定义的时候的形参next,传入的实参实参上是dispatch(i+1), 递归调用dispatch. 并保证结果返回的是个Promise
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}