- 狼书(卷2):Node.js Web应用开发
- 狼叔
- 2777字
- 2025-02-20 23:08:43
2.1.1 什么是中间件
中间件是框架的扩展机制,主要用于抽象HTTP请求过程。在单一请求响应过程中加入中间件,可以更好地应对复杂的业务逻辑。
如果把一个HTTP处理过程比作污水处理,那么中间件就像一层层的过滤网。每个中间件在HTTP处理过程中通过改写请求和响应数据、状态,实现了特定的功能。大家都知道HTTP是无状态协议,所以HTTP请求的过程可以这样理解:请求被发送过来,经过无数中间件拦截,直至被响应为止。
启动服务器的流程如图2-1所示。

图2-1
看到此处,你是否能够理解什么是中间件呢?从代码层面来看,在App和server.listen中间的都是中间件,过滤的先后顺序和挂载中间件的顺序有关,越靠前的中间件越早执行。
大多数实例代码都是以log中间件为例的,这里稍微解释一下,如图2-2所示。

图2-2
请求到达服务器后,依次经过各个中间件,直至被响应,所以整个流程会流经log中间件,由请求响应(业务逻辑)中间件给予响应,具体描述如下。
○ 请求到达log中间件,记录此时的时间。
○ 放过,执行next,此时会执行下一个中间件。
○ 执行到请求响应中间件,通过ctx.body向浏览器输出响应结果。
○ 当响应回到log中间件时,根据当前时间减去请求到达时间,打印请求耗时。
○ 最后把响应写到浏览器里。
➘ Koa v1
著名的Express框架的作者TJ Holowaychuk(TJ),对于回调地狱也无计可施,恰巧ES6的Generator出现并改变了大家对异步流程控制的无奈态度。当ES6在Chrome V8中实现后,社区开始积极接受ES新特性,这直接促成了Koa框架的产生。ES6里一边循环一边计算的机制称为生成器,其最大的特点就是可以通过yield关键字交出函数的执行权(暂停执行),即可以让一个函数异步执行完成后,再继续当前流程。
Koa v1是一个探索版本,是在Node.js最开始支持ES6 Generator特性时使用Generator/yield做的大胆尝试。很明显,它是成功的,代码在很大程度上比之前更容易理解,这一切都得益于基于ES6 Generator的流程控制。
让我们来看一下Koa v1的示例。

在app.js文件里,代码如下。

对上述代码的说明如下。
○ 这里的app.use和Express里的app.use是一样的,都是挂载中间件。
○ function*(next){}是基于Generator的Koa v1中间件的写法,此处是重点。
○ app.listen用于指定启动端口。
在《狼书(卷1)》中我们讲过yield转让执行权,其实yield next前面的代码是对req进行处理的,yield next后面的代码是对res进行处理的。这里的next和Express里的next一样,都用于执行后面的中间件。
执行$node app.js,访问http://127.0.0.1:3000/,控制台日志输出如下。

从日志中我们可以看出执行顺序,具体如下。
○ 先经过logger中间件[logger middleware] before yield...。
○ 然后跳转到[response middleware] response...。
○ 然后执行[logger middleware] after yield...。
很明显,这3个输出在logger中间件的yield next前后进行了处理,通过yield next将执行权转交给了下一个中间件,即response中间件,于是代码执行方式就借助Generator转变成了顺序执行,这就是Koa v1带给我们的最直观的变化。

简单点说,一切都要感谢ES6中的Generator。Koa v1约定所有中间件的写法都以ES6 Generator为准,这样可以避免回调地狱。即使你现在不熟悉ES6 Generator也没关系,在Koa v2里已经不推荐ES6 Generator中间件写法了,而且从warning日志中可以看出,在Koa v3中很有可能会移除对Genrator的支持。
另外,需要注意的是,ES6 Generator中间件内部使用this作为隐式的上下文,这在JavaScript里是非常容易用错的,这也算是Koa v1带来的不可避免的小瑕疵吧!
➘ Koa v2
Koa v2是一个更成熟的Web框架,是由Koa v1演进而成的。Koa v2最重要的特性是支持async函数,并且同时支持如下3种不同类型函数的中间件。
○ common function middleware:通用函数中间件,是最常见的,也称modern middleware。
○ generator function middleware:生成器函数中间件,简单来说,它与Generator/yield的功能是类似的。
○ async function middleware:async函数中间件,利用了最潮的async函数的特性,async/await是异步流程控制的终极神器,故为当下的使用趋势。
除了掌握基本用法,最好还能理解为什么要支持这3种中间件,以及它们各自适用的场景。
1.通用函数中间件
先看一下最常见的通用函数中间件的示例,如下。

(ctx,next)=>{}是基于ES6箭头函数简写的中间件,它只有两个参数,ctx是上下文,包括所有的请求和响应;而next()是确定当前中间件是否将请求向下一个中间件放行的函数。对应ES5版本的写法如下。

(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。
关于Node.js的版本,通用函数中间件最好使用Node.js v4.0以上版本,尽管这种中间件写法没有对Node.js的版本做出特别要求,但Koa内部代码具有很多ES6特性。使用通用函数中间件最好的一点是可以支持Promise。从整体来看,通用函数中间件是最容易上手的,写法的难度系数也是3种中间件里最低的。如果有Express使用经验,或者想从Express项目迁移到Koa项目,可以考虑从通用函数中间件开始学习,学习成本很低。
2.生成器函数中间件
Koa的设计初衷是便于开发者基于ES6 Generator来构建更好的流程控制,所以Koa v2也支持ES6 Generator,但和Koa v1稍有不同。我们先来看看Koa v2的生成器函数中间件代码,稍后详细解释。

将以上代码与Koa v1代码对比,主要区别如下。
○ Koa v2生成器函数中间件被co.wrap包裹并被转换成了function*(){}。
○ Koa v2生成器函数中间件的参数中多了ctx,和上面的通用函数中间件一样,上下文ctx被显式声明了,统一了写法。
○ 从Koa v2生成器函数中间件跳转到下一个中间件是通过yield next()函数实现的,而不是Koa v1中用的yield next。
在Koa v2支持的3种中间件中,生成器函数中间件写法与Koa v1的中间件写法是最像的。如果要从Koa v1迁移到Koa v2,通过生成器函数中间件来迁移是最佳的。同样地,我们可以在Generator里进行yieldable操作,完成更强大的异步流程控制功能。
3.async函数中间件
Koa之所以从v1升级到v2,可以说很大一部分原因在于async函数。async函数在是ES6里引入的(async函数进入了ES7-stage3,但没有进入ES7的最终规范),可以非常好地解决异步问题,相当于更高级的自带执行器的Generator。
async函数的优势如下。
○ 语义更好。
○ 无须执行器,比Generator+co的解决方案强很多。
○ await可以无缝调用异步Promise方法,更好地向后兼容。
下面给出async函数中间件的用法。

以上代码的要点如下。
○ async函数除了多一个async关键字之外,和普通函数无异,同样支持箭头函数(Generator是不支持箭头函数的)。
○ 与async函数搭配的await关键字可以对接Promise方法,尽管它没有yield的yieldable那么强大,但也足够用。
○ Koa v2的async函数中间件跳转到下一个中间件时需要使用await next(),和通用函数中间件里的return next()类似。
总结一下,从形式上看,async函数中间件无疑是所有中间件中最耀眼的那个,它语义清楚,结合await关键字可以非常好地整合Promise,还能更好地兼容各种已有代码。在目前最新的Node.js LTS版本中,async函数的性能已经和Promise一样好了,推荐大家使用。
目前Koa主要有两个版本,中间件有4种写法,总结如下,见表2-1。
表2-1

从表2-1中可以看出各种写法的应用场景。如果从用户的角度来分析,大致可以总结出以下3种经验。
○ 如果是新手,推荐使用async函数中间件。
○ 如果熟悉Express或Promise,推荐先使用通用函数中间件,然后使用async函数中间件。
○ 如果之前使用的是Generator,建议放弃它,直接转到async函数中间件。