05月05, 2018

koa源码简析

koa源码简析

Koa代码量非常少,大致分为5个部分,

  • application.js (koa对象)
  • context.js (koa上下文对象)
  • request.js (request对象)
  • response.js (response对象)
  • koa-compose/index.js (合并中间件)

与原生node模块写法的对比

首相我们对比一下使用node原生的http模块和koa构建服务的异同

//原生的http模块
let http = require('http');
http.createServer();

http.on('request',function (req,res) {
  res.write("hello world");
  res.end();
})

http.listen(3000);


//koa
let Koa = require('koa');
const app = new Koa();

app.use(async function (ctx,next) {
  ctx.body = 'hello'
  next();
});
app.use(async function (ctx,next) {
  ctx.body =  ctx.body + 'world';
  next();
});

app.listen(3000);

koa的构造方法

第一步我们使用Koa构造方法新建了一个app对象,koa构造方法中只有不到10行代码,且全部是用来进行初始化工作。

constructor() {
    super();
    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);//context为koa上下文对象的构造方法
    this.request = Object.create(request);//request 为request对象的构造方法
    this.response = Object.create(response);//response为response对象的构造方法
}

然后调用的use方法来加载中间件,use方法也很简单,检验中间是否为函数后,如果是普通函数时,会把普通函数转换成GeneratorFunction,然后把中间件放入中间件数组middleware中。

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;

listen方法

然后是listen方法,listen方法调用了callback()方法,作为回调函数传入了http.createServer中,根据我们上边使用原生的node的http模块搭建服务时,callback会是一个处理req请求信息,然后使用res返回响应信息的函数。

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

callback方法

app.callback就是整个文件的核心方法,整个服务器对于请求和响应的处理都在体现在这个方法里头。

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

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

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
}

koa-compose

首先它使用koa-compose模块把多个中间件经行了组合,这个方法也是koa洋葱模型的具体实现,点开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!');
  }

  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, function next() {
          return dispatch(i + 1);
        }))
      } catch (err) {
        return Promise.reject(err);
      }
    }
  }
}

这个方法刚开始看起来有点无从下手,建议使用DEBUG的方式,使用两三个中间来测试它,然后跟着源码走,这样理解起来比较好。现在我们使用三个中间件来测试它。

const compose = require('koa-compose');

const middleware1 = async function (ctx,next) {
  console.log("middleware1 >>");
  next();
  console.log("<< middleware1");

};
const middleware2 = async function (ctx,next) {
  console.log("middleware2 >>");
  next();
  console.log("<< middleware2")
};
const middleware3 = async function (ctx,next) {
  console.log("middleware3 >>");
  next();
  console.log("<< middleware3")
};

let middlewares = [middleware1,middleware2,middleware3];
let fn = compose(middlewares);
fn("1");
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, function next() {
      return dispatch(i + 1);
    }))
  } catch (err) {
    return Promise.reject(err);
  }
}

第一个中间件走到dispatch方法   i=0,index=-1,所以 i>index   index = 0,fn = middleware[0]第一个中间件   然后就返回一个Promise对象,这个Promise对象的,并执行我们的第一个中间件,还把dispatch作为next传入了中间件中,如果此时,我们在中间里头调用next()相当于在dispatch方法中递归调用自己。

第二,三个同样走第一个的流程,到了第四个时,此时   i=3,index=2,所以 i>index   if (i === middleware.length) fn = next; 而递归传进来的next为空(return dispatch(i + 1)没有next参数)   if (!fn) return Promise.resolve();放回空的成功。   中间件流程执行完毕

createContext

最后回到我们的koa对象中的callback方法中,经过中间件包装以后,判断是否出错,出错对错误进行处理(打印和返回),然后创建ctx对象

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

这个方法很简单,都是一些赋值初始化操作,这了也可以看到,每一次请求的ctx,request,response都是新创建的

handleRequest

最后就是放回handleRequest函数给http.createServer方法作为node原生的http模块的请求处理方法。至于处理resreq的细节,这里就不详细解释,koa的重点不在这,koa也暴露了这些交给开发者自行处理。

文章推荐

写完之后,发现有一篇写得非常详细的文章

本文链接:https://www.qiangshuidiyu.xin/post/koa-source-code.html

-- EOF --

Comments