express, koa, redux三者中间件对比

这三者对各自的中间件有着不同的实现,作者本人对此也比较好奇,在这里小小的研究一下源码,探究三者之间的异同

什么是中间件

在我看来,中间件就是在你的代码运行中进行一些修改的工具。比如你想喝水,那么喝水之前你将水净化就可以理解为是一次中间件的执行。他不是插件,独立于程序之外,而更像是在你的代码中表现一种类似连接的功能

Koa 与 Express 中间件概述

这两者都是Node层面的,这里我们根据官方文档来对比

Express

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var app = express();

// 没有挂载路径的中间件,应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});

// 挂载至 /user/:id 的中间件,任何指向 /user/:id 的请求都会执行它
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method);
next();
});

// 路由和句柄函数(中间件系统),处理指向 /user/:id 的 GET 请求
app.get('/user/:id', function (req, res, next) {
res.send('USER');
});

可以看到express的中间件是使用next进行线性调用的,一个接着一个的执行,是一种尾递归的调用(后文会讲)。然后在最后一个中间件中进行对response的处理(习惯)

Koa

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
const Koa = require('koa');
const app = new Koa();

// x-response-time

app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});

// logger

app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response

app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

从代码中的await可以看出,koa的中间件绝对不是线性的,因为一旦使用了await,代码就会停止当前中间件的执行转而去执行await后面的代码,这里next表示下一个中间件。所以这是一个支持generator的洋葱圈模型(后文会讲)

koa洋葱圈

Koa 与 Express 中间件源码进一步解析

上面提到,express的中间件是尾递归调用,而koa的中间件因为使用了await所以是支持generator的洋葱圈模型,这里以此展开来分析代码

Express

我们直接进入application.js中观察中间件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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方法就能不断传递到下一个中间件。在函数的末尾递归调用了next方法,所以称为尾递归调用

Koa

Koa对中间件的处理是在一个独立的包koa-compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict'

module.exports = compose

function compose (middleware) {

return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

Koa中使用了Promise来支持异步,这里不停调用dispatch.bind(null, i + 1)传递下一个中间件,一个一个中间件向里执行,直到最后一个中间件执行完resolve掉,然后不断向前resolve中间件,直到第一个中间件被resolve。我们可以发现,相应的处理并不在中间件中而是在其resolve

Redux

对于redux的基础createStorereducerdispatch等就不解释了,这里直接看applyMiddleware的代码

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
import compose from './compose'

export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}

const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

这里还是比较好理解的,middlewareAPI中包含两个api,一个是storegetState;另一个是覆写的dispath,这是一个外部变量,最终指向覆写后的dispach,对于compose的作用是compose(f, g, h) 返回 () => f(g(h(..args)))

那么 dispatch = compose(...chain)(store.dispatch)即原生的 store.dispatch 传入最后一个“中间件”,返回一个新的 dispatch , 再向外传递到前一个中间件,直至返回最终的 dispatch, 当覆写后的dispatch 调用时,每个“中间件“的执行又是从外向内的”洋葱圈“模型

Author: AddOneG
Link: http://yoursite.com/2018/09/14/express-koa-redux三者中间件对比/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.