koa源码解析

1,571 阅读6分钟

使用node原生http模块创建server

const http = require('http');

function callback(req, res) {};
const server = http.createServer(callback);
// 使用事件监听执行callback
server.on('request', callback);
server.listen(3000);

使用node中的原生http模块很容易创建server,仅仅需要调用createServer方法,指定指定当接收到客户端请求时所需执行的callback即可。在callback中,使用两个参数,一个是http.IncomingMessage对象,此处代表一个客户端请求,另一个是http.ServerResponse对象,代表一个服务器端响应对象。如果createServer中不传入callback,也可以使用server监听request事件执行callback。然后调用listen方法指定server需要监听的端口、地址等。

server.listen(port, [host], [backlog], [callback]);

listen方法中,可使用4个参数,其中后三个参数是可选参数。port参数值用于指定需要监听的端口号,参数值为0时将为HTTP服务器随机分配一个端口号,HTTP服务器将监听来自于这个随机端口号的客户端连接。host参数用于指定需要监听的地址,如果省略该参数,服务器将监听来自于任何IPv4地址的客户端连接。backlog参数值为一个整数值,用于指定位于等待队列中的客户端连接的最大数量,一旦超越这个长度,HTTP服务器将开始拒绝来自于新的客户端的连接,该参数的默认参数值为511。callback参数来指定listening事件触发时调用的回调函数,该回调函数中不使用任何参数。如果不在listen方法中传入callback参数,也可以使用事件监听。

server.on('listening', () => {});

使用koa创建server

const Koa = require('koa');

const app = new Koa();
app.use(async (ctx, next) => {
	console.log('enter middleware');
	await next();
	console.log('out middleware');
});
app.listen(3000);

使用koa创建server与使用node原生http模块创建server差别很大,但我们知道koabase node.jsweb框架。因此,其底层使用的node.js的技术,只是在此基础上进行了进一步的包装,从而可以使用各种独立功能的中间件操作上下文、操作reqres对象,实现请求解析,响应返回。

koa源码

koa源码主要逻辑包括两个部分,一部分是koa本身的逻辑,主要进行服务的创建、ctxreqres这三个对象的管理和操作。另一部分就是其特有的洋葱模型的中间件流程控制,主要是koa-composekoa-compose源码解析)中间件实现的该功能。

koa关键目录
koa的源码都在lib目录下的四个文件。application.jskoa的入口文件,主要目标是创建服务,context.jsctx上下文对象文件,request.js是对req对象进行处理的文件,response.js是对res进行处理的文件。下面我们从两个方面对源码进行阅读,首先是服务创建阶段即初始化阶段,其次是请求处理阶段。

初始化阶段

初始化阶段包括koa初始化,该部分初始化主要是实例话koa对实例化的app进行属性和方法的挂载。server初始化,这部分的初始化主要是创建server并指定请求到底是需要执行的操作。在代码表现就是:

// koa初始化
const app = new Koa();
// server初始化
app.listen(3000, () => {
	console.log('server running: http://localhost:3000');
});
koa初始化

现在我们把不必要的代码去掉看看koa初始化做了哪些事情。

module.exports = class Application extends Emitter {
	constructor(options) {
		super();
		options = options || {};
		this.proxy = options.proxy || false;
	    this.subdomainOffset = options.subdomainOffset || 2;
	    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
	    this.maxIpsCount = options.maxIpsCount || 0;
	    this.env = options.env || process.env.NODE_ENV || 'development';
	    if (options.keys) this.keys = options.keys;
	    this.middleware = [];
	    this.context = Object.create(context);
	    this.request = Object.create(request);
	    this.response = Object.create(response);
	    if (util.inspect.custom) {
	      this[util.inspect.custom] = this.inspect;
	    }
	}
	listen(...args) {}
	toJSON() {}
	inspect() {}
	use(fn)() {}
	callback() {}
	handleRequest(ctx, fnMiddleware) {}
	createContext(req, res) {}
	onerror(err) {}
};

koa的初始化,主要是为app挂载各种属性和方法,如上所示,在constructor主要是属性挂载,其中包括我们经常使用的context上下文对象,request对象,response对象,middleware中间数组,env环境参数、proxy代理操作等。并在prototype上挂载了许多方法,其中handleRequestcreateContextonerror这三个方法是私有方法,主要是提供给callback方法使用的,目的分别是handleRequest通过compose的中间件对request进行处理,createContext是创建context上下文对象,并在上下文挂载state对象提供给视图进行数据传递使用,onerror主要是进行全局对错误。其他5个个方法分别作用是:listen初始化通过node的原生http模块createServertoJSONinspect主要是对app上的subdomainOffset, proxy, env三个属性通过only进行提取。use函数是对中间件函数进行管理,主要是pushmiddleware数组中,并return this进行链式调用,callback主要是return handleRequest指定了当接收到客户端请求时所需执行的操作。

server初始化

server初始化主要是createServer并指定server.on('request', callback)中的callback。这里面主要就是执行了实例化后的applisten函数。下面我们看一下这个listen函数的具体内容。

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

看到listen源码其实就是跟我们使用node原生的http模块创建server一样了。createServer并指定监听的地址和端口号。其中this.callback指定了接收请求后执行的操作。下面我们看一下这个callback函数的具体内容。

callback() {
	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;
}

callback使用闭包return handleRequest进行请求到底的处理,如何进行请求处理我们在请求处理阶段进行详细介绍。其中compose(this.middleware)this.on('error', this.onerror)分别是对中间价进行compose流程控制和注册全局error事件处理函数。

请求处理阶段

当一个请求过来时,它会进入到callback回调函数返回的handleRequest函数中进行处理。

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

handleRequest主要执行了两部分操作,一部分是调用私有方法createContext创建上下文对象,一部分是调用私有方法handleRequest进行请求处理。

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.state = {};
   return context;
}
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);
}

createContext主要是创建上下文对象也就是中间件入参中的ctx对象,并为contextrequestresponse对象挂载各种属性。handleRequest对请求进行处理,该处理操作是通过compose以后的中间件实现的也就是fnMiddleware操作的。我们知道koa-compose处理以后的中间件返回的是一个匿名函数这里对应的是fnMiddleware,通过该函数对请求处理返回的是promise,然后进行请求resolvereponse收尾处理,或rejectcatch收尾处理。其中response是在koa中定义的私有方法。

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

  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

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

  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.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);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

其他

对于context.jsrequest.jsresponse.js这三个文件都只是简单的module.exports = {}对外暴露一个对象进行contextrequestresponse操作并不是很复杂,有兴趣的同学可以直接看这三个文件的源码(koa源码)。