1、koa是什么?
官方网站如是说:Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。
Express
Express是第一代最流行的web框架,它对Node.js的http进行了封装,用起来如下:
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
问题: Express是基于ES5的语法,实现异步代码的方法是回调。如果异步嵌套层次过多,就会陷入可怕的回调地狱。
koa 1.0
随着新版Node.js开始支持ES6,该团队基于ES6的generator
重新编写了下一代web框架koa1.0。使用generator实现异步,代码看起来像同步的
var koa = require('koa');
var app = koa();
app.use('/test', function *() {
yield doReadFile1();
var data = yield doReadFile2();
this.body = data;
});
app.listen(3000);
koa2
ES7草案引入了新的关键字async
和await
,可以轻松地把一个function变为异步模式。该团队又与时俱进,基于当时还是草案的ES7开发了koa2。
Koa 依赖 node v7.6.0 或 ES2015及更高版本和 async 方法支持.
// app.js
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('收到一个请求')
await next()
ctx.body = 'Hello World';
})
app.listen(3000);
到此时,我们明白koa2的由来,并且可以启动一个koa2的web服务(运行此命令 node app.js
)。
2、koa2中间件机制
很多博文介绍koa2会引用以下这张洋葱图,这张图告诉我们,当服务接受到一个Request,会依此从外层往里执行,输出响应Response的过程是从内往外执行。对应图中的业务,执行顺序依次是: Request -> Registry Manager -> Status Code Redirect -> Error Handler -> Cache -> Session -> Routes -> Pylons App
响应输出的过程如下: Pylons App -> Routes -> Session -> Cache -> Error Handler -> Status Code Redirect -> Response
洋葱的每一层都可以理解为一个中间件层,执行相对应的业务逻辑,那么是如何实现的呢?下面进入本文的重点。
// app.js
const Koa = require('koa')
const app = new Koa()
// 中间件midw1
function midw1 (ctx, next) {
console.log('from midw1 前')
next()
console.log('from midw1 后')
}
// 中间件midw2
function midw2 (ctx, next) {
console.log('from midw2 前')
next()
console.log('from midw2 后')
}
function process (ctx) {
console.log('from core process')
}
app.use(midw1)
app.use(midw2)
app.use(process)
app.listen('7000')
// 在终端运行node app.js
// 当浏览器发起一个http://localhost:7000/ 请求时,输出如下:
// from midw1 前
// from midw2 前
// from core process
// from midw2 后
// from midw1 后
通过上述这个简短的demo可知:1. koa中间件的执行严格按照use注册顺序,最先注册的,最先获取Request,但是最后执行完,比作洋葱的最外层。2. 当执行过程中遇到next,就开始执行里层的中间件。next将每一个中间件的执行分为两段,从当前执行上下文转移到里层。如下图:
3、compose 的 v1.0
本文的重点为通过next实现koa的洋葱圈执行顺序,涉及到封装ctx的省略。根据上文的分析,不难推出,通过app.use
方法,将中间件函数依次注册到一个队列。代码如下:
function midw1 (next) {
console.log('from midw1 前')
next()
console.log('from midw1 后')
}
// 中间件midw2
function midw2 (next) {
console.log('from midw2 前')
next()
console.log('from midw2 后')
}
function process () {
console.log('from core process')
}
class App {
midware = []
use(fn) {
if (typeof fn === 'function') {
this.midware.push(fn)
} else {
throw new Error('fn必须是函数')
}
}
}
const app = new App()
app.use(midw1)
app.use(midw2)
app.use(process)
console.log(app.midware)
// [ [Function: midw1], [Function: midw2], [Function: process] ]
那么如何将app.midware中的函数实现遇到next就跳去执行下一个函数,直到执行到最后一个函数,再依次往前执行剩余部分。所以想到的第一个实现方法就是将app.midware中第i+1个函数作为第i个函数的next参数传入,于是就有了以下第一个版本
function compose(midwareArr) {
let i = 0
let fn = midwareArr[i]
let next = midwareArr[++i]
fn(next)
}
compose(app.midware)
// from midw1 前
// from midw2 前
// TypeError: next is not a function
4、compose 的 v2.0
第一个版本实现了从第1层到第2层,那么如何将第3层的中间件传到第2层呢?第n+1个中间件函数传到第n层呢?这里要上一个高阶函数,注意了
function compose(midwareArr) {
let i = 0
function dispatch(i) {
let fn = midwareArr[i]
if (!fn) {
return
}
// next = function(){dispatch(i)}
return fn(function(){
dispatch(i+1)
})
}
return dispatch(i)
}
compose(app.midware)
// from midw1 前
// from midw2 前
// from core process
// from midw2 后
// from midw1 后
再次运行demo,输出已经实现预期了。这里细细品......
5、compose 的 v3.0
走到这里,万里长征差不多就走了一大半了。还有一些细节需要完善。如果同一个中间件函数里有多个next呢?测试代码如下。
function midw1 (next) {
console.log('from midw1 前')
next()
console.log('from midw1 后')
next()
console.log('from midw1 第二个next后')
}
// 中间件midw2
function midw2 (next) {
console.log('from midw2 前')
next()
console.log('from midw2 后')
}
function process () {
console.log('from core process')
}
class App {
midware = []
use(fn) {
if (typeof fn === 'function') {
this.midware.push(fn)
} else {
throw new Error('fn必须是函数')
}
}
}
const app = new App()
app.use(midw1)
app.use(midw2)
app.use(process)
function compose(midwareArr) {
let i = 0
function dispatch(i) {
let fn = midwareArr[i]
if (!fn) {
return
}
return fn(function(){
dispatch(i + 1)
})
}
return dispatch(i)
}
compose(app.midware)
输出如下,midw2、process 中间件执行了两次。第二次遇到next,又执行了一次function({dispatch(i)}
from midw1 前
from midw2 前
from core process
from midw2 后
from midw1 后
from midw2 前
from core process
from midw2 后
所以这里我们需要记录一下当前中间件已经执行过一次next了,再次执行要报错。改进代码如下:
function compose(midwareArr) {
let i = 0
let flag = []
function dispatch(i) {
let fn = midwareArr[i]
if (!fn) {
return
}
if (flag[i]){
throw new Error('next 只能用一次')
}
flag[i] = true
return fn(function(){
dispatch(i + 1)
})
}
return dispatch(i)
}
这个时候再测试一下同一中间件出现多个next的用例,OK了。再坚持一下,马上结束了哈哈。
6、终极版
next返回的是Promise实例,终极版出炉
function compose(midwareArr) {
let i = 0
let flag = []
function dispatch(i) {
let fn = midwareArr[i]
if (!fn) {
return Promise.resolve()
}
if (flag[i]){
return Promise.reject(new Error('next 只能用一次'))
}
flag[i] = true
return Promise.resolve(fn(function(){
return dispatch(i + 1)
}))
}
return dispatch(i)
}
compose(app.midware)
我们再用以下代码测试一次,到此为止,已经模拟实现了koa2的中间件执行方法compose
async function a (next) {
console.log('a')
let res = await next()
console.log('aa')
console.log(res)
}
function b(next) {
console.log('b')
next()
console.log('bb')
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('jjjjjj')
}, 3000);
})
}
function c (next) {
console.log('中心')
next()
}
class App {
midware = []
use(fn) {
if (typeof fn === 'function') {
this.midware.push(fn)
} else {
throw new Error('fn必须是函数')
}
}
}
const app = new App()
app.use(a)
app.use(b)
app.use(c)
7、一起看看源码吧
下面是koa中compose函数实现的源码,源码是通过index记录next是否执行,i表示的是当前中间件在midware中的索引。
function compose(midware) {
let index = -1
function dispatch(i) {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'))
}
index = i
let fn = midware[i]
if (!fn) {
return Promise.resolve()
}
return Promise.resolve(fn(function () {
return dispatch(i + 1)
}))
}
return dispatch(0)
}
compose(midware)
8、总结
今天我又博学了,hahaha!收到一个陌生人的点赞,开心了半天。我会继续努力的。如有错误,欢迎指正。