中间件是框架额扩展机制,主要用于 HTTP 请求过程。在单一请求响应过程中加入中间件,可以更好地应对负责的业务逻辑。
从代码层面看,在 App 和 serverListen 之间的代码都是中间件,中间件执行的顺序和挂载的顺序有关,越靠前的中间件越早执行。下面是中间件执行的经典洋葱图。
请求到达服务器后,依次经过各个中间件,直至被响应。
Koa v1
ES6 中一边循环一边计算的计算机制成为生成器,其最大的特点就是可以通过 yield 关键字交出函数的执行权(暂停执行),即可以让一个函数异步执行完后,再继续执行当前流程。
Koa v1 是一个探索版本,实在 Node.js 最开始支持 ES6 Generator 特性时使用 Generator/yield 做出的大胆尝试。
在 app.js 文件里,代码如下:
const koa = require('koa')
const app = koa()
// 日志
app.use(function * log(next) {
const start = new Date()
yield next
const ms = new Date() - start
console.log(ms)
})
// 响应
app.use(function *res(next) {
this.body = 'hello world'
})
app.listen(3000)
很明显,在执行了 yield next 以后,才会执行下一个中间件,即 res 中间件,于是代码执行方式就借助 Generator 转变成了顺序执行,这就是 Koa v1 带来的最直观的变化。
另外,ES6 Generator 中间件内部使用 this 作为隐式的上下文,这在 js 中非常容易出错,这算是 Koa v1 的小小瑕疵。
Koa v2
Koa v2 是一个更成熟的 Web 框架,是由 Koa v1 演进而成的。Koa v2 最重要的特性就是支持 async 函数,并且同时支持如下 3 种不同类型函数的中间件。
- 通用函数中间件
下面是通用函数中间件的示例
const Koa = require('koa')
const app = new Koa()
// 日志
app.use((ctx, next) =>{
const start = new Date
return next().then( () => {
const ms = new Date - start
console.log(ms)
} )
})
// 响应
app.use(ctx = > {
ctx.body = 'hello world'
})
(ctx, next) => {} 和 Koa v1 中间件的 function *(next) {} 相比,区别如下。
- 写法差异:Koa v2 中间件是普通的函数,不是 Generator 函数。
- 参数差异:Koa v1 中间件的参数有 1 个,而 Koa v2 中间件的参数有 2 个,这主要是因为上下文 ctx 在 Koa v2 被显式的声明了。可以理解,Koa v1 中间件里的 this 就是 Koa v2 中间件里的 ctx,它们的 API 是完全一样的。
- 异步技术差异:Koa v2 中间件内部使用 Promise 进行异步处理,而 Koa v1 中间件内部使用 Generator。
- 生成器函数中间件
Koa 的设计初衷是便于开发者基于 ES6 Generator 来构建更好的控制流程,所以 Koa v2 也支持 ES6 Generator,但和 Koa v1 稍有不同。
const Koa = require('koa')
const app = new Koa()
// 日志
app.use(co.warp()function*(ctx, next) {
const start = new Date
yield next()
const ms = new Date - start
console.log(ms)
})
// 响应
app.use(ctx => {
ctx.body = 'hello world'
})
app.listen(3000)
将以上代码与 Koa v1 代码对比,主要区别如下:
- Koa v2 生成器函数中间件被 co.warp 包裹并被转换成 function*(){}。
- Koa v2 生成器函数中间件的参数多了 ctx,和上面的通用函数中间件一样,上下文 ctx 被显式的声明了,统一了写法。
- 从 Koa v2 生成器函数中间件跳转到下一个中间件是通过 yield next() 函数实现的,而不是 Koa v1 中用的 yield next
- async 函数中间件
async 函数的优势如下:
- 语义更好
- 无须执行器,比 Generator + co 的解决方案好很多。
- await 可以无缝调用异步 Promise 方法,更好的向后兼容
下面给出 async 函数中间件的用法:
const Koa = require('koa')
const app = new Koa()
// 日志
app.use(async (ctx, next) => {
const start = new Date
await next()
const ms = new Date - start
console.log(ms)
})
// 响应
app.use(ctx => {
ctx.body = 'hello world'
})
app.listen(3000)
以上代码的要点如下:
- async 函数除了多一个 async 关键子之外,和普通函数无异,同样支持箭头函数。(Genertaor 是不支持箭头函数的)
- 与 async 函数搭配的 await 关键字可以对接 Promise 方法,尽管他没有 yield的 yieldable那么强大。
- Koa v2 的 async 函数中间件跳转到下一个中间件时需要使用 await next(), 和普通函数中间件里的 return next() 类似。
总结,从形式上看,async 函数中间件无疑是所有中间件中最耀眼的那个,它语义清楚,结合 await 关键字可以非常好的整合 Promise,还能更好地兼容各种已有代码。