KOA核心源码解析

在开发Web应用中,往往离不开web应用框架,Koa由于其轻量级、高简洁度以及表达力强的特性成为开发时较优的选择之一。Koa框架的所有功能都是通过插件来实现的没有中间件的捆绑,它本身的代码只有一千多行而已。
 
目前已有许多文章和教程讲述如何使用Koa进行项目搭建,也有koa-generator等插件可以一键生成服务器,本文就不做赘述。然而当我们快速使用Koa进行服务端程序编写时,是Koa的哪些核心功能为我们带来了便利?这些核心功能又是如何实现的呢?下面将浅析Koa的源码来加深大家的理解。
 
一、 Koa代码构成

KOA核心源码解析

其中request.js和response.js是对node的res对象进行进一步的封装,请求对象(req)和响应对象(res)这两个参数用来封装自己的request对象和response对象,并新定义了一些方法和属性,上下文对象再将方法委托给它们,在服务端完成数据的处理。
 
1. application.js:
const util = require('util');const only = require('only');class Application extends Emitter {  @api public  constructor(){    super();    //设置是否获取真正的客户端ip地址,因为在请求过程中可能会使用很多的代理服务器    this.proxy = false;    // 设置子域名的偏移量    this.subdomainOffset = 2;    this.env = process.env.NODE_ENV || 'development';    if (util.inspect.custom) {      this[util.inspect.custom] = this.inspect;    }  }  toJSON() {   // node-only模块设置只暴露这三个属性    return only(this, ['subdomainOffset', 'proxy', 'env']);  }    inspect() {    return this.toJSON();  }}
一个请求跳转越多的代理服务器,那么X-Forwarded-For头部的ip地址就会越多,第一个是原始的客户端请求,后续的都是代理服务器ip,Koa的proxy属性设置为true即是采用X-Forwarded-For的第一个地址,反之则使用server中的socket.remoteAddress属性值,并且当proxy为true时,Koa的protocol属性还会判断是否是https协议。
二、Koa中间件
Koa实现了中间件流程控制来让我们简单的使用中间件,我们先来看看Koa的use方法做了些什么:
use(fn) {  // 判断是否是函数  if (typeof fn !== 'function'throw new TypeError('middleware must be a function!');  // 判断是否是generator函数  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'    );    // 因为koa1使用的是Generator函数, koa2使用的是async函数, 所以做此处理    fn = convert(fn);  }  //往中间件容器添加中间件  this.middleware.push(fn);  //返回当前实例,支持链式调用中间件  return this;}
再来看看Koa是如何操作中间件流程控制的,这种独特的操作方式又被称为“洋葱模型”,
function compose(middleware) {  // 返回了一个函数,接受context和next参数,koa在调用koa-compose时只传入context,所以此处next即为undefined;  return function(context, next) {    let index = -1;    //  从第一个中间件开始执行    return dispatch(0);    function dispatch(i) {      //  在一个中间件执行两次next函数时,抛出异常      if (i <= index) return Promise.reject(new Error('next() called multiple times'));      //  设置index,作用是判断在同一个中间件中是否调用多次next函数.      index = i;      let fn = middleware[i];      //  跑完所有中间件时,fn = next ,即fn = undefined,可以理解为终止条件;      if (i === middleware.length) fn = next;      if (!fn) return Promise.resolve();      try {        //  返回一个定值的promise对象.值为下一个中间件的返回值。        //  这里是最核心的逻辑,递归调用下一个中间件,并将返回值返回给上一个中间件。        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));      } catch (err) {        return Promise.reject(err);      }    }  };}
顾名思义,洋葱模型可以理解成一个请求从外到里一层层的经过中间件,而响应的时候则是从里到外再一层层的经过中间件。我们可以写简单的代码段测试如下:
const m1 = async (context, next) => {  console.log('in-1');  await next();  console.log('out-1');};const m2 = async (context, next) => {  console.log('in-2');  await next();  console.log('out-2');};const m3 = async (context, next) => {  console.log('in-3');  await next();  console.log('out-3');};compose([m1, m2, m3])();//output// in-1// in-2// in-3// out-3// out-2// out-1
三、 context对象:
Koa的context对象主要是将上下文的访问器和方法直接委托给它们的 request 对象和 response 对象,在源码中,这一“委托”操作是这样实现的:delegate API创建一个delegator实例,把proto接收到的一些操作委托给它的prop属性去处理
delegate(proto,'response')  .method('attachment')  .method('redirect')  .method('remove')  .method('vary')  .method('set')  .method('append')  .method('flushHeaders')  .access('status')  .access('message')  .access('body')  .access('length')  .access('type')  .access('lastModified')  .access('etag')  .getter('headerSent')  .getter('writable')
了解了这些核心点后我们再梳理一下整个流程就会发现流畅许多,引用一张流程图来更直观的理解Koa是如何运作的:

KOA核心源码解析

这样大家对Koa的核心流程就有了更细节的认知,后续无论是个人搭建项目或是遇到相关问题时也能更加流畅的解决。

始发于微信公众号: 腾讯DeepOcean

 

发表评论