提到 Node.js,就不得不提目前炙手可热的两大框架 Express 和 Koa,他俩都是 NodeJs 的主流开发框架。
Express是基于 Node.js 平台,快速、开放、极简的 Web 开发框架。
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
就现在的版本,koa
和express
最大的区别也只有两点,这也是koa
的最大的两个优点:
koa
比express
轻量的多,koa
更像是一个中间件框架,只是一个基础的架子,需要用到的相应的功能时,用相应的中间件来实现就好,比如路由系统、静态资源托管等等。koa
是返回 promise, express
是 void。所以koa
的 next() 可以用 await 修饰,被 await 修饰的 next() 会等待上一个中间件执行完毕后再执行自身的 next()。express
是基于回调来处理,至于回调到底有多么的不好用?
// 原始使用回调版本
app.use((req, res, next) => {
console.log('start');
next();
// 在这里,读取文件1,2的内容,并合并写入到3中
fs.readFile('./file1.txt', (err1, data1) => {
if (err) {
console.log('failed in file1')
}
console.log(data1.toString());
fs.readFile('./file2.txt', (err2, data) => {
if (err2) {
console.log('failed in file2')
}
console.log(data2.toString());
fs.writeFile('./file3.txt', data1 + data2, err3 => {
if (err3) {
console.log('failed in file3')
}
console.log('done');
});
});
});
});
Koa
NoCallback! No Callback!No Callback!重要的事情说三遍!
koa1
基于co库,所以koa1
利用Generator来代替回调,而koa2
由于node7.6.0对async/await的支持,所以koa2
利用的是async/await。
Koa1
和 Koa2
中间件的实现思路是一样的,只是实现方式有所区别。koa1
的中间件使用 generator 函数,使用 yield next 进入下一个中间件,koa2
中间件使用 async 函数,通过await next()进入下一个中间件。
首先,中间件并不是 express/koa 独有的概念,所以广泛意义的中间件的意义是:
中间件是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。
将上面的话翻译成人话就是:
在一次消费过程(在 koa 中可以看成是一次接受请求到响应结束的过程)中被调用的函数,就属于中间件。
中间件函数能够访问请求对象 (req)、响应对象(res) 以及应用程序的请求/响应循环中的下一个中间件函数,下一个中间件函数通常由名为 next 的变量来表示。
想象一下当业务逻辑复杂的时候,为了明确和便于维护,需要把处理的事情分一下,分配成几个部分来做,而每个部分就是一个中间件。
Koa
的中间件模型就是洋葱圈模型。Koa
的每个中间件就像是一个洋葱圈,每次当有一个请求进入的时候,每个中间件都会被执行两次。
每次执行下一个中间件传入两个参数 ctx 和 next,参数 ctx 是由 koa
传入的封装了 request 和 response 的变量,可以通过它访问 request 和 response,next 就是下一个要进入执行的中间件。
其实很明显,在koa
的中间件中,通过 next 函数,将中间件分成了两部分,next 上面的一部分会首先执行,而下面的一部分则会在所有后续的中间件调用之后执行。当一个中间件独调用 next() 则该函数暂停,并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件将恢复执行其上游行为。
当我们使用koa
进行开发的时候,因为读取数据库或是http请求等都是异步请求,所以我们为了保证洋葱模型会使用号称异步终极解决方案的async/await。
const app = new Koa()
// 中间件A
app.use(async (ctx, next) => {
console.log("A1")
await next()
console.log("A2")
});
// 中间件B
app.use(async (ctx, next) => {
console.log("B1")
await next()
console.log("B2")
});
// 中间件C
app.use(async (ctx, next) => {
console.log("C1")
await next()
console.log("C2")
});
app.listen(3000);
// 输出
// A1 -> B1 -> C1 -> C2 -> B2 -> A2
当程序运行到await next()的时候就会暂停当前中间件,进入下一个中间件,处理完之后才会再回过头来继续处理。也就是说,当一个请求进入,中间件1会被第一个和最后一个经过,中间件2则是被第二和倒数第二个经过,依次类推。
例如,我们经常需要计算一个请求的响应时间,在 Koa 中, 我们可以在中间件的开始记录初始时间,当响应返回时,代码执行又回到了原来的中间件,此时根据当前时间和初始时间的时间差便得到了响应时间。
function responseTime() {
return async function responseTime(ctx, next) {
const start = Date.now()
await next() // wait for other middleware to run
const delta = Math.ceil(Date.now() - start)
ctx.set('X-Response-Time', delta + 'ms')
})
}}
主要用来对比 koa 和 express 的执行顺序
let A = '';
app.use(async (ctx, next) => {
A = await new Promise((resolve, reject) => {
fs.readFile('./a.txt', function(err1, data1) {
resolve(data1.toString());
});
});
console.log('1');
await next();
console.log('1 call');
});
app.use(async (ctx, next) => {
console.log('2');
await next();
console.log('2 call');
});
app.use(async (ctx, next) => {
await new Promise((resolve, reject) => {
fs.writeFile('./b.txt', A, function(err) {
if(err) {
reject(err);
}
resolve();
});
});
console.log('3');
next();
const B = await new Promise((resolve, reject) => {
fs.readFile('./b.txt', function(err, data) {
if(err) {
reject(err);
}
resolve(data.toString());
});
});
console.log('3 call ',B);
});
// 1 => 2 => 3 => 3 call => 2 call => 1 call
// 1 => 2 => 2 call => 1 call => 3 => 3 call
每个中间件都接收了一个next参数,在 next 函数运行之前的中间件代码会在一开始就执行,next函数之后的代码会在内部的中间件全部运行结束之后才执行。
要想达到上面洋葱圈的运行效果,我们需要做什么呢?
我们带着这样的一个思路,再回头来看Koa是如何实现的:
3. this.middleware是中间件集合的数组
4. koa-compose模块的 compose 方法用来构建执行顺序
完美!下面只需要具体分析一下它们分别做了什么就可以了
// middleware用来保存中间件
app.use = (fn) => {
this.middleware.push(fn)
return this
}
// compose组合函数来规定执行次序
function compose (middleware) {
// context:上下文,next:传入的接下来要运行的函数
return function (context, next) {
function dispatch (i) {
// 中间件
let fn = middleware[i]
if (!fn) return Promise.resolve()
try {
// 我们这边假设和上文中的例子一样,有A、B、C三个中间件
// 通过dispatch(0)发起了第一个中间件A的执行
// A中间件执行之后,next作为dispatch(1)会被执行
// 从而发起了下一个中间件B的执行,然后是中间件C被执行
// 所有的中间件都执行了一遍后,执行Promise.resolve()
// 最里面的中间件C的await next()运行结束,会继续执行console.log("C2")
// 整个中间件C的运行结束又触发了Promise.resolve
// 中间件B开始执行console.log("B2")
// 同理,中间件A执行console.log("A2")
return Promise.resolve(fn(context, () => {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}
Koa 利用了在中间件中间传入 next 参数的方法,再结合 middleware 中间件数组和 compose 组合函数,构建了洋葱圈的中间件执行结构。
完!