# 中间件与洋葱模型
洋葱模型在像 Koa, egg, midway等 node 框架中被广泛运用。如下图就是洋葱模型:
# 什么是 AOP
AOP
为 Aspect Oriented Programming 的缩写,中文意思为:面向切面编程,它是函数式编程 (opens new window)的一种衍生范式。
假如我想把一个苹果(源数据)处理成果盘(最终数据)我该怎么做?
1.苹果(源数据) ----> 2. 洗苹果 ----> 3.切苹果 ---->4.放入盘子 ----> 5.果盘(最终数据)
共有 5 个步骤,如果我想升级一下果盘,打算在切苹果之前先削皮,放入盘子后摆成五角星形状那么我的步骤应该如下:
1.苹果(源数据) ----> 2.洗苹果 ----> 3.削皮 ----> 4.切苹果 ----> 5.放入盘子 ----> 6.摆成五角星形状 ----> 7.果盘(最终数据)
上面每个步骤都可以看成相应的方法,步骤 3 和 6 加入与否都不影响我制作出果盘这个结果,可以看出这样是非常灵活的
其实这就是生活中面向切面编程的例子, 换句话说,就是在现有程序中,加入或减去一些功能不影响原有的代码功能。
什么是
# 洋葱模型
洋葱模型其实就是中间件处理流程,中间件的生命周期大致有:
- 前期处理
- 交给并等待其他中间件处理
- 后期处理
多个中间件处理,就形成了所谓的洋葱模型,是 AOP 面向切面编程的一种应用。
以 Koa.js 为例:
const Koa = require('koa');
const app = new Koa();
// 中间件1
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
// 中间件 2
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.listen(8000, '0.0.0.0', () => {
console.log(`Server is starting`);
});
// 输出结果为:
// 1
// 3
// 4
// 2
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
在 koa
中,中间件被 next()
方法分成了两部分。next()
方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。
在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session
处理等等。其处理顺序先是 next()
前请求(Request
,从外层到内层)然后执行 next()
函数,最后是 next()
后响应(Response
,从内层到外层),也就是说每一个中间件都有两次处理时机。
# Why 使用洋葱模型
假如没有洋葱模型,当我们的中间件依赖于其他中间件的逻辑时,该如何处理呢?
比如我们需要知道一个请求或者操作 db 的耗时是多少,而且想要获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。
app.use(async(ctx, next) => {
const start = new Date();
await next();
const delta = new Date() - start;
console.log (`请求耗时: ${delta} MS`);
console.log('拿到上一次请求的结果:', ctx.state.baiduHTML);
})
app.use(async(ctx, next) => {
// 处理 db 或者进行 HTTP 请求
ctx.state.baiduHTML = await axios.get('http://baidu.com');
})
app.listen(9000, '0.0.0.0', () => {
console.log(`Server is starting`);
});
// 输出结果为可能为:
// 请求耗时:758 MS
// 拿到上一次请求的结果:{
// status:200
// ...
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果没有洋葱模型,这就很难实现了。
# 深入洋葱模型
还是以 koa 为例,分析一下内部的实现。
# app.use 方法
use 方法做了一件事,维护得到的 middleware 中间件数组。
use(fn) {
// ...
// 维护中间件数组——middleware
this.middleware.push(fn);
return this;
}
2
3
4
5
6
# listen 方法和 callback 方法
执行 app.listen
方法的时候,其实是 Node.js
原生 http
模块 createServer
方法创建了一个服务,其回调为一个 callback 方法。callback 方法中有一个非常重要的函数:compose 函数,它的返回是一个 Promise 函数。
listen(...args) {
debug('listen');
// node http 创建一个服务
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback(){
const fn = compose(this.middleware); // 返回值是一个函数
const handleRequest = (req, res) => {
// 创建 ctx 上下文环境
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn); // 将 ctx 放入 fn 中处理并返回处理结果
};
return handleRequest;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
handleRequest 中会执行 compose 函数中返回的 Promise 函数并返回结果。
handleRequest(ctx, fn) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 执行 compose 中返回的函数,将结果返回
return fn(ctx).then(handleResponse).catch(onerror);
}
2
3
4
5
6
7
8
9
# compose 函数
基本逻辑:
// 函数处理的数据
let context = {}
function middleware_01 (cxt) {
console.log('1')
middleware_02(cxt)
console.log('2')
}
function middleware_02 (cxt) {
console.log('3')
console.log('4')
}
// 调用中间件 compose 函数
function compose () {
// 默认调用第一个中间件
middleware_01(context)
}
compose()
// 1
// 3
// 4
// 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
基本逻辑是实现了,但每次都要指示调用的函数名字,不够灵活。
参考 koa-compose
这个库中的compose
函数引。其实现如下所示:
这里先不考虑一些特殊边界情况,将其简易化,完成基本的功能实现。
function compose (middleware){
// ...
return function (context,next){
// 一开始的时候传入为 0,后续会递增
return dispatch(0)
function dispatch (i){
// 获取当前中间件
let fn=middleware[i]
// 当 fn 为空时表示已经执行到最后一个中间件
// dispatch 递归回调结束,从当前中间件开始执行 next() 后面部分的代码
if (!fn) return Promise.resolve()
// 执行中间件,第一个是上下文,第二个是 next 函数。
// 也就是说中间件执行 next 的时候也就是调用 dispatch 函数的时候
return Promise.resolve(fn(conntext, function next () { dispatch(i + 1) })
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
具体流程:
第一次执行:fn 为第一个中间件函数。传入当前上下文,以及一个 next函数(执行下一个 dispatch)。
第二次执行:dispatch 传入参数为 2, fn 为空,这时候执行 if (!fn) return Promise.resolve()
,然后执行当前中间件 next()
之后的代码,然后依次往上,从而形成了洋葱模型。
实现:
const middleware = []
let mw1 = async function (ctx, next) {
console.log("next前,第一个中间件")
await next()
console.log("next后,第一个中间件")
}
let mw2 = async function (ctx, next) {
console.log("next前,第二个中间件")
await next()
console.log("next后,第二个中间件")
}
let mw3 = async function (ctx, next) {
console.log("第三个中间件,没有next了")
}
function use(mw,middleware) {
middleware.push(mw);
}
function compose(middleware) {
return (ctx, next) => {
return dispatch(0);
function dispatch(i) {
const fn = middleware[i];
if (!fn) return;
return fn(ctx, dispatch.bind(null, i+1));
}
}
}
use(mw1,middleware); // 压入执行栈
use(mw2,middleware);
use(mw3,middleware);
const fn = compose(middleware)(); // 组合中间件
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
34
35
# 总结
Koa
的洋葱模型指的是以 next()
函数为分割点,先由外到内执行 Request
的逻辑,再由内到外执行 Response
的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是 compose
方法。