一、why Koa
Koa是由Express原班人马打造,但是相较于Express的大而全,Koa是小而精的。Koa没有绑定很多的框架以及插件,更容易让我们进行扩展,包括现在较为流行的EggJSandThinkJS都是基于Koa开发的。Koa避免了Express中间件基于callback形式的调用,它使用了我们JS新版本特性,Koa1中间件借助于我们的coandgenerator特性,Koa2借助了Promiseandasync await特性,更好的进行流程控制以及catch我们的错误。Koa提供了Context对象,实际上是对我们node中requestandresponse对象的封装,我们不需要很多的手动处理我们的requestandresponse对象。Context是贯穿我们整个请求的过程,我们可以中间件需要传递参数挂在到Context对象上。(栗子:我们可以将用户信息挂在它上面,通过ctx.state.user进行操作。)Koa的中间件执行机制:洋葱圈模型。它不是按顺序执行的,多个中间件会形成一个先进后出的栈结构,当前中间件掌握下一个中间件的执行权,对于流程控制以及后置处理逻辑的实现非常有效。
二、Koa2与Koa1的比较
- 中间件的管理方式:
Koa1借助coandgenerator管理我们的中间件,Koa2借助async await(async函数返回的是Promise对象)管理我们的中间件。 context对象获取:Koa1通过this对象(this.req,this.res)获取,Koa2通过ctx参数(ctx.req,ctx.res)获取。- 社区成熟度:
Koa2的轮子多且成熟,生态比Koa1丰富。
三、举个栗子,剖析源码
1. 栗子
listen方法创建了我们的http服务器,端口是3000。并且通过use方法传入了三个koa中间件, 并且console出我们的执行结果。
2. 从listen方法说起
- 首先我们去github上clone最新的Koa的源代码。然后源码结构如下:
- 可以看得出来整个源码的核心代码皆在
lib文件,我们在栗子中require('koa')实际上是引入的lib/application.js里面的Application类。下面我们来分析一下我们的listen方法的实现,首先我们看一下这个Application类里面到底有些什么:
- 我们可以看到
Application类下有我们的listen方法,方法如下:
这个this.callback()也就是我们createServer的入参,这个参数是一个函数,是作为 request 事件的监听函数,这个函数还接收两个参数req、res,也就是我们的请求对象以及响应对象。那我们可以推断出这个listen方法里面的this.callback()实际上也返回是一个函数,这个函数接收req、res两个入参。
- 接下来我们再看一下这个
this.callback()里的callback方法是啥样的:
这个方法接收两个参数: 一个是我们的compose的返回结果fn、另一个是我们的ctx对象,这个方法主要是对我们请求的处理以及错误的统一捕获以及处理。
3. 执行一下我们的栗子:扒一扒中间件的执行机制
Koa的中间件执行机制有个形象的称呼-洋葱圈模型。我们先执行栗子代码,看看输出的结果到底是什么?
4. 从理解use方法开始
5. 中间件执行机制的核心就是compose
再回到上文提到的callback函数,我们的中间件执行机制的核心就是compose(this.middleware),下面我们来分析一下compose函数的源码:
- 先校验我们middleware的参数正确性,是否是数组,数组项是否为函数;
- compose函数实际上是返回一个function,这个返回的function,在上面也提及,最终是传入到handleRequest方法中然后传入ctx参数:fnMiddleware(context)。
- 再回到我们的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)));
})
- 仔细阅读这个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)
}
}
}
四、总结
- Koa这个框架是小而精美的,它的源码是也非常少,只有一千多行。中间件的执行机制主要是读懂这个koa-compose的代码,多个中间件会形成一个
先进后出的栈结构,当前中间件掌握下一个中间件的执行权。 - Koa本身的功能可能是满足不了我们日常开发的需求的,我们可以通过许多的第三方的包,也可以自定义中间件来辅助我们的开发。(了解了中间件机制,自定义一个中间件就非常容易了)
Koa1借助coandgenerator管理我们的中间件,Koa2借助async await(async函数返回的是Promise对象)管理我们的中间件。
五、Koa2实战
这是一个全栈开发实战实例:koa2-mysql-sequelize-JWT(供参考交流,一起学习)。
六、前端修炼指南(希望可以对您有帮助)
这是一个个人博客(前端修炼指南):front-end-Web-developer-interview