【进阶全栈-02】:Koa源码分析(中间件执行机制、Koa2与Koa1比较)

·  阅读 729
【进阶全栈-02】:Koa源码分析(中间件执行机制、Koa2与Koa1比较)

一、why Koa

  1. Koa是由Express原班人马打造,但是相较于Express的大而全,Koa是小而精的。Koa没有绑定很多的框架以及插件,更容易让我们进行扩展,包括现在较为流行的EggJS and ThinkJS都是基于Koa开发的。
  2. Koa避免了Express中间件基于callback形式的调用,它使用了我们JS新版本特性,Koa1中间件借助于我们的co and generator特性,Koa2借助了Promise and async await特性,更好的进行流程控制以及catch我们的错误。
  3. Koa提供了Context对象,实际上是对我们node中request and response对象的封装,我们不需要很多的手动处理我们的request and response对象。Context是贯穿我们整个请求的过程,我们可以中间件需要传递参数挂在到Context对象上。(栗子:我们可以将用户信息挂在它上面,通过ctx.state.user进行操作。)
  4. Koa的中间件执行机制:洋葱圈模型。它不是按顺序执行的,多个中间件会形成一个先进后出的栈结构,当前中间件掌握下一个中间件的执行权,对于流程控制以及后置处理逻辑的实现非常有效。

二、Koa2与Koa1的比较

  1. 中间件的管理方式Koa1借助co and generator管理我们的中间件,Koa2借助async await(async函数返回的是Promise对象)管理我们的中间件。
  2. context对象获取Koa1通过this对象(this.req,this.res)获取,Koa2通过ctx参数(ctx.req, ctx.res)获取。
  3. 社区成熟度Koa2的轮子多且成熟,生态比Koa1丰富。

三、举个栗子,剖析源码

1. 栗子

image.png
这段代码呢,通过listen方法创建了我们的http服务器,端口是3000。并且通过use方法传入了三个koa中间件, 并且console出我们的执行结果。

2. 从listen方法说起

  1. 首先我们去github上clone最新的Koa的源代码。然后源码结构如下:

image.png

  1. 可以看得出来整个源码的核心代码皆在lib文件,我们在栗子中require('koa')实际上是引入的lib/application.js里面的Application类。下面我们来分析一下我们的listen方法的实现,首先我们看一下这个Application类里面到底有些什么:

image.png

  1. 我们可以看到Application类下有我们的listen方法,方法如下:

image.png
可以看出这里实际上还是通过我们Node.js的http模块,通过createServer创建一个http服务器, 然后listen方法接收我们的端口,这和我们使用原生Node.js创建一个http服务器是一样的。

这个this.callback()也就是我们createServer的入参,这个参数是一个函数,是作为 request 事件的监听函数,这个函数还接收两个参数req、res,也就是我们的请求对象以及响应对象。那我们可以推断出这个listen方法里面的this.callback()实际上也返回是一个函数,这个函数接收req、res两个入参。

  1. 接下来我们再看一下这个this.callback()里的callback方法是啥样的:

image.png
callback方法内的第一句fn = compose(this.middleware),这个this.middleware是我们在Application类中的constructor就定义了,是一个数组,这里实际上存储的就是我们举的栗子中的app.use()的入参方法,也就是我们Koa的中间件。这个compose方法很重要,主要是控制我们的中间件的执行,下文讲中间件的执行机制会仔细说说这个compose。然后看这个函数返回,果然和我们推断的一样,这个callback方法最终返回的是一个handleRequest方法,接收req,res两个参数。handleRequest方法中,第一句this.createContext方法实际上就是对我们req、res对象的封装:
image.png
通过createContext 创建了我们ctx对象。这个callback方法返回的handleRequest方法第二句是返回了Application类上的handleRequest方法的执行结果(注意这里有“重名”的方法,注意区分),方法如下:
image.png

这个方法接收两个参数: 一个是我们的compose的返回结果fn、另一个是我们的ctx对象,这个方法主要是对我们请求的处理以及错误的统一捕获以及处理

3. 执行一下我们的栗子:扒一扒中间件的执行机制

Koa的中间件执行机制有个形象的称呼-洋葱圈模型。我们先执行栗子代码,看看输出的结果到底是什么?     

image.png

4. 从理解use方法开始

image.png
app.use实际上就是调用了Applcation类下面的use方法,这个方法主要是先判断入参形式是否是一个函数等(参数校验),如果入参是一个generator函数,这也就是koa1中的写法,在koa2中需要koa-convert去转换一下。然后将我们入参传入的中间件函数push到this.middleware数组中

5. 中间件执行机制的核心就是compose

再回到上文提到的callback函数,我们的中间件执行机制的核心就是compose(this.middleware),下面我们来分析一下compose函数的源码:

image.png

  1. 先校验我们middleware的参数正确性,是否是数组,数组项是否为函数;
  2. compose函数实际上是返回一个function,这个返回的function,在上面也提及,最终是传入到handleRequest方法中然后传入ctx参数:fnMiddleware(context)。
  3. 再回到我们的compose中return的这个function,接收两个参数:第一个就是我们handleRequest方法传入的ctx对象,第二个next呢,实际上是传入的一个方法,这个方法是在所有middleware执行完毕后,最后执行处理的函数。这个function核心就是递归执行我们的middleware。要理解这段代码,先要理解Promise.reslove()
// Promise.reslove返回一个fulfillled状态的promise对象
// 可以看成new Promise()的快捷方式

Promise.reslove(fn(context, dispatch.bind(null, i+1)));
// 实际上是等于
new Promise((relove, reject) => {
  reslove(fn(context, dispatch.bind(null, i+1)));
})
复制代码
  1. 仔细阅读这个function, 它是先定义了一个index(利用我们的闭包每次执行一次dispatch方法去改变index),执行了dispatch(0),这个dispatch方法就是我们执行机制实现的核心,dispatch函数里面如果没有执行到最后一个middleware,就返回了Promise.reslove(fn(context, dispatch.bind(null, i + 1))), 这个fn(context, dispatch.bind(null, i + 1))也就是执行我们通过app.use加入的middleware函数,middleware函数统一接收两个参数一个是context,一个是next:下一个middleware函数,这样可以看出来如果我们koa中某个中间件没有执行next方法,那么之后加入的中间件是不会执行的。这也就形成了我们的洋葱圈模型
// 核心方法:递归调用我们middlewares, 基于Promise进行异步流程控制;
// Promise.resolve()返回的是一个thenable对象;
// 所以我们koa2中中间件都基于async函数,await等待下个中间件方法的执行;
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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
复制代码

四、总结

  1. Koa这个框架是小而精美的,它的源码是也非常少,只有一千多行。中间件的执行机制主要是读懂这个koa-compose的代码,多个中间件会形成一个先进后出的栈结构,当前中间件掌握下一个中间件的执行权
  2. Koa本身的功能可能是满足不了我们日常开发的需求的,我们可以通过许多的第三方的包,也可以自定义中间件来辅助我们的开发。(了解了中间件机制,自定义一个中间件就非常容易了)
  3. Koa1借助co and generator管理我们的中间件,Koa2借助async await(async函数返回的是Promise对象)管理我们的中间件

五、Koa2实战

这是一个全栈开发实战实例:koa2-mysql-sequelize-JWT(供参考交流,一起学习)。

六、前端修炼指南(希望可以对您有帮助)

这是一个个人博客(前端修炼指南):front-end-Web-developer-interview

六、参考文档

  1. Koa2 还有多久取代 Express
  2. Koa vs Express && Koa1 vs Koa2
分类:
前端
标签:
分类:
前端
标签: