如果你有 express ,koa, redux 的使用经验,就会发现他们都有 中间件(middlewares)的概念,中间件 是一种拦截器的思想,用于在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作。
最开始接触 中间件是在服务端使用 express 和 koa 的时候,后来从服务端延伸到前端,看到其在redux的设计中也得到的极大的发挥。中间件的设计思想也为许多框架带来了灵活而强大的扩展性。
本文主要对比redux, koa, express 的中间件实现,为了更直观,我会抽取出三者中间件相关的核心代码,精简化,写出模拟示例。示例会保持 express, koa,redux 的整体结构,尽量保持和源码一致,所以本文也会稍带讲解下express, koa, redux 的整体结构和关键实现:
示例源码地址, 可以一边看源码,一边读文章,欢迎star!
本文适合对express ,koa ,redux 都有一定了解和使用经验的开发者阅读
服务端的中间件
express 和 koa 的中间件是用于处理 http 请求和响应的,但是二者的设计思路确不尽相同。大部分人了解的express和koa的中间件差异在于:
express采用“尾递归”方式,中间件一个接一个的顺序执行, 习惯于将response响应写在最后一个中间件中;
而koa的中间件支持 generator, 执行顺序是“洋葱圈”模型。
所谓的“洋葱圈”模型:
不过实际上,express 的中间件也可以形成“洋葱圈”模型,在 next 调用后写的代码同样会执行到,不过express中一般不会这么做,因为 express的response一般在最后一个中间件,那么其它中间件 next() 后的代码已经影响不到最终响应结果了;
express
首先看一下 express 的实现:
入口
// express.js var proto = require('./application'); var mixin = require('merge-descriptors'); exports = module.exports = createApplication; function createApplication() { // app 同时是一个方法,作为http.createServer的处理函数 var app = function(req, res, next) { app.handle(req, res, next) } mixin(app, proto, false); return app }
这里其实很简单,就是一个 createApplication 方法用于创建 express 实例,要注意返回值 app 既是实例对象,上面挂载了很多方法,同时它本身也是一个方法,作为 http.createServer的处理函数, 具体代码在 中:
// application.js var http = require('http'); var flatten = require('array-flatten'); var app = exports = module.exports = {} app.listen = function listen() { var server = http.createServer(this) return server.listen.apply(server, arguments) }
这里 app.listen 调用 nodejs 的http.createServer 创建web服务,可以看到这里 var server = http.createServer(this) 其中 this 即 app 本身, 然后真正的处理程序即 app.handle;
中间件处理
express 本质上就是一个中间件管理器,当进入到 app.handle 的时候就是对中间件进行执行的时候,所以,最关键的两个函数就是:
尾递归调用中间件处理 req 和 res
添加中间件
全局维护一个stack数组用来存储所有中间件,app.use 的实现就很简单了,可以就是一行代码 ``
// app.use app.use = function(fn) { this.stack.push(fn) }
express 的真正实现当然不会这么简单,它内置实现了路由功能,其中有 router, route, layer 三个关键的类,有了 router 就要对 path 进行分流,stack 中保存的是 layer实例,app.use 方法实际调用的是 router 实例的 use 方法, 有兴趣的可以自行去阅读。
app.handle 即对 stack 数组进行处理
app.handle = function(req, res, callback) { var stack = this.stack; var idx = 0; function next(err) { if (idx >= stack.length) { callback('err') return; } var mid; while(idx < stack.length) { mid = stack[idx++]; mid(req, res, next); } } next() }
这里就是所谓的"尾递归调用",next 方法不断的取出stack中的“中间件”函数进行调用,同时把next 本身传递给“中间件”作为第三个参数,每个中间件约定的固定形式为 (req, res, next) => {}, 这样每个“中间件“函数中只要调用 next 方法即可传递调用下一个中间件。
之所以说是”尾递归“是因为递归函数的最后一条语句是调用函数本身,所以每一个中间件的最后一条语句需要是next()才能形成”尾递归“,否则就是普通递归,”尾递归“相对于普通”递归“的好处在于节省内存空间,不会形成深度嵌套的函数调用栈。有兴趣的可以阅读下阮老师的尾调用优化
至此,express 的中间件实现就完成了。
koa