浅谈 Web 前端中间件两种模型与实现

中间件是什么?

首先它是一个组件, 其次, 它与具体的业务逻辑是解耦的, 业务逻辑经常需要调用底层逻辑或者一些系统通用逻辑, 然而每次都需要去编写这些接口和代码是没有必要的, 这时候中间件就可以出场了.

具体的场景是可以看做如下: 前端业务需要某个服务, 我无需关心提供这项服务的具体底层逻辑, 只需要通过中间件获取到结果就好了

具体的例子

我们开发了一个很简单 webserver, 现在需要把不同 url 的请求路由到不同的业务处理 模块, 一开始我们的请求路径很少, 直接用 switch 语句就可以应付了, 后面随着路径越来越多, 每次都要改这个往这个 switch 加 case, 后面需求越来越变态了, 竟然要根据url路径中的参数(如用户 id 或者商品 id)路由到不同的 handler 方法, 显然我们的路由逻辑越来越复杂了

作为一个开发者, 关于如何处理好的路由的显然不是我最关心的(我只想好好地写业务代码), 那么这时候如果有一个第三方(路由中间件), 只需要传入请求的匹配规则, 对应的 handler 函数, 至于如何把请求路由到正确的 handler 中, 那是路由中间件的事情了, 之后就可以愉快的写业务代码了

中间件的模型与机制

1. 洋葱模型 (代表作 koa)

首先来两张egg.js 官网的原理图帮助简单明地去理解
洋葱模型:
中间件执行顺序:

通过上面的图看以看出, 所有的请求都会经过中间件两次, 先依次的进入中间件, 遇到 yield 退出来,执行下一个中间件, 反过来一个个往上面冒泡执行中间件yield 后面的代码, 这样子好处就是可以非常简单实现了后置处理逻辑, 比如想对返回的结果加一层封装就可以很简单的实现了

下面来学习一下如何去实习,
提前了解几个知识点:

Generator http://es6.ruanyifeng.com/#docs/generator
Promise http://es6.ruanyifeng.com/#docs/promise
Thunk函数 http://es6.ruanyifeng.com/#docs/generator-async#Thunk-函数

可以点击去回顾温习一下

简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* f1() {
console.log('f1: pre next');
yield* f2();
console.log('f1: post next');
}

function* f2() {
console.log(' f2: pre next');
yield* f3();
console.log(' f2: post next');
}

function* f3() {
console.log(' f3: pre next');
console.log(' f3: post next');
}

var g = f1();
g.next();

输出结果为:

1
2
3
4
5
6
f1: pre next
f2: pre next
f3: pre next
f3: post next
f2: post next
f1: post next

再看看 koa 实现的源码

1
2
// 很简单就一行
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));

也太简单了吧, 继续深入挖掘一下, 我们这里再重现一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  compose 的作用就是把 compose([f1, f2, ..., fn])转化为fn(...f2(f1(noop())))
function compose(middleware) {
return function*(next) {
var i = middleware.length;
var prev = next || noop();
var curr;
while (i--) {
curr = middleware[i];
prev = curr.call(this, prev);
}
yield* prev;
}
}
var co = require('co');
co(compose([f1, f2, f3]));

看到这里可能会问 co 是什么? https://github.com/tj/co 可以看一下官网的 readme, 同时源代码很少很精巧. co 是一个函数库, 可以帮助我们不用自动执行 generator 函数, 不需要我们手动执行 next() 方法.

有两种方式可以自动执行 Generator

  1. Thunk
  2. Promise 对象

co 模块其实就是将两种自动执行器集合到一起去了(Thunk 函数和 Promise 对象)
下面分别用用 Thunk 函数简单的实现 co 的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Promise 版本的 co
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
// Thunk 版本的 co
function co(fn) {
var gen = fn();

function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}

next();
}

var gen = function* (){
var f1 = yield Promise.resolve('f1');
var f2 = yield Promise.resolve('f2');
// 还支持数组的写法, 可以处理并发的异步操作,全部完成之后才会下一步
// var result = yield [
// Promise.resolve(1),
// Promise.resolve(2)]
console.log(f1.toString());
console.log(f2.toString());
};
co(gen);

小结:

使用 洋葱模型的 中间件很多, 比较有名就是 koa 的, 阿里出品的 egg 也是基于这个模式的, 前端的 Redux 中间件也是基于这个模型的, 本质上就是捕捉store.dispatch(action), 然后在dispatch 前后执行一些操作,比如记录日志

2. 瀑布流模型 (代表作 connect )

虽然 “洋葱模型” 的中间件功能很强大, 也很符合中间件的设计逻辑, 但是”瀑布流模型”(connect) 结合 express中间件理念的设计, 我还是更加喜欢这种瀑布流方式的中间件设计方法

具体的代码可以参考 https://github.com/senchalabs/connect#readme connect 是一个中间件的库 , express 的中间件就是这个东西

在 express 中, 一个完整个应用就是各种调用中间件

1
2
3
4
5
6
7
8
app.use(function(request,response,next){
if(request.url === '/'){
response.writeHead(200,{"Content-Type":"text/plain"});
response.end("This is home\n");
} else {
next();
}
})

而这种调用方式也是十分优美的, 一个接着一个,顺序调用, 这与 express 框架的设计哲学相辅相成.
实现起来没有洋葱模型的 koa 那么炫酷,比较朴素 ,源码有284行, 精简一下,中间件相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 中间件本身是一个函数
function middleware(req,res,next){
// do something;
// 做完后调用下一个函数
next();
}
function express() {
var funcs = []; // 待执行的函数数组, 这里可以拓展 funcs 为多层级对象,
//可以借用中间件来实现路由的功能.
var app = function (req, res) {
var i = 0;
function next() {
var task = funcs[i++]; // 取出函数数组里的下一个函数
if (!task) { // 如果函数不存在,return
return;
}
task(req, res, next); // 否则,执行下一个函数
}
next();
}
/**
* use方法就是把函数添加到函数数组中
* @param task
*/
app.use = function (task) {
funcs.push(task);
}
return app; // 返回实例
}

简单的来说express 的 connect 中间件功能并不入如洋葱模型的 中间件强大, 但是优势是结合了 express 框架的基于中间件运行的理念 , 在开发使用上面也能够得心应手

小结

本篇文章是最近了解 koa 和 egg.js 之后, 对 Web前端Server框架学习与探索 文中中间件部分的一个展开了解, 在学习的过程中, 也更加深刻的理解 generator 异步编程以及基于co 这个库一些异步编程应用