koa源码解析

使用 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 的源码都在 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源码 )。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章