使用中间件解耦业务流函数

前置条件

  • 听说过AOP(面向切面编程),了解中间件、洋葱模型等概念
  • 了解Redux、Koa的中间件的使用
  • 一点函数式编程技巧(方便阅读redux源码)

背景

点击按钮,执行分析,打印结果。

1
2
3
4
5
const add = (a, b) => a + b;
const onClick = () => {
const result = add(1, 1);
console.log(result);
}

新增需求 ,在分析时增加loading。

1
2
3
4
5
6
const onClick = () => {
console.log('loading...');
const result = add(1, 1);
console.log(result);
console.log('loaded');
}

这时候,loading和业务代码就已经耦合了。像loading、log等是最简单的耦合情况。

实际开发中会发生更复杂的耦合。比如在某个分析前后需要控制图层的加载。

1
2
3
4
5
6
const onClick = () => {
console.log('如果有白模,卸载白模');
const result = add(1, 1);
console.log(result);
console.log('如果卸载过白模,加载白模');
}

继续新增需求,在分析前要矫正参数,在分析后要过滤结果。

1
2
3
4
5
6
7
8
const onClick = () => {
console.log('loading...');
console.log('矫正参数');
const result = add(1, 1);
console.log('过滤结果');
console.log(result);
console.log('loaded');
}

这样整个函数就会越来越难以维护。

期望

loading函数应该和分析函数解耦,且形态大致如下:

1
2
3
4
5
const loading = async (next) => {
console.log('loading...');
await next();
console.log('loaded');
}

loading函数不关心分析函数的内容,它只关心执行的顺序,在分析前后显示loading。

onClick函数也会对应修改为:

1
2
3
4
5
6
7
const onClick = () => {
const result = applyMiddlewares(
loading,
add
)(1, 1);
console.log(result);
}

联想

从loading函数的形态,很容易能联想到Redux或者Koa的中间件。

Redux

1
2
3
4
5
6
const logger = store => next => action => {
console.log('dispatching', action)
const result = next(action)
console.log('next state', store.getState())
return result
}

Koa

1
2
3
4
5
6
const logger = async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
}

代码示例均来官方文档。

核心

因为redux的中间件必须和store绑定使用,不能直接使用,所以参考redux源码和[koa源码][https://github.com/koajs/compose/blob/master/index.js]实现了`applyMiddlewares`方法。

1
2
3
4
5
6
7
8
9
10
const applyMiddlewares =
(...middlewares) =>
async (ctx) => {
const chain = middlewares.reduceRight(
(acc, curr) => () => curr(ctx, acc),
() => {}
);
await chain();
return ctx;
};

函数的作用就是控制执行顺序。

输入是中间件函数middlewares和上下文ctx,输出是上下文ctx

中间件

现在把loading改造成为中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const middleware1 = async (ctx, next) => {
console.log("middleware1 before");
await next();
console.log("middleware1 after");
};

const middleware2 = async (ctx, next) => {
console.log("middleware2 before");
await next();
console.log("middleware2 after");
};

const middleware3 = async (ctx, next) => {
console.log("middleware3 before");
await next();
console.log("middleware3 after");
};

把分析函数更新为异步函数,再套一层函数成为中间件。

1
2
3
4
5
const add = (a, b) => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(a + b), 3000);
});
};
1
2
3
4
5
6
const core = async (ctx, next) => {
const { a, b } = ctx;
const result = await add(a, b);
console.log(result);
ctx.result = result;
};

参数和结果都在ctx中获取和赋值。

核心函数(最后一个函数)可不再调用next()

调用

调用时,传入中间件函数

1
2
3
4
5
6
7
const ctx = await applyMiddlewares(
middleware1,
middleware2,
middleware3,
core
)({ a: 1, b: 1 });
console.log(ctx); // { a: 1, b: 1, result: 2}

控制台日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
middleware1 before ​​​​​at ​​​​​​quokka.js:8:3​

middleware2 before ​​​​​at ​​​​​​quokka.js:14:3​

middleware3 before ​​​​​at ​​​​​​quokka.js:20:3​

2 ​​​​​at ​​​​​​​​result​​​ ​quokka.js:28:3​

middleware3 after ​​​​​at ​​​​​​quokka.js:22:3​

middleware2 after ​​​​​at ​​​​​​quokka.js:16:3​

middleware1 after ​​​​​at ​​​​​​quokka.js:10:3​

{ a: 1, b: 1, result: 2 }
​​​​​at ​​​​​​​​ctx​​​ ​quokka.js:49:1

延伸

有了中间件之后,处理新的需求就变得非常简单。

middleware1作为loading函数,middleware2作为矫正参数,middleware3作为过滤结果即可。

像定位和高亮结果,也都可以作为中间件函数。

洋葱图:(矫正参数((((分析)过滤结果)定位)高亮))

代码:

1
2
3
4
5
6
7
8
const ctx = await applyMiddlewares(
矫正参数,
定位,
高亮,
过滤结果,
分析
)({ a: 1, b: 1 });
console.log(ctx);

执行顺序:矫正参数=》分析=》过滤结果=》定位=》高亮

附件

完整示例代码

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const add = (a, b) => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(a + b), 3000);
});
};

const middleware1 = async (ctx, next) => {
console.log("middleware1 before");
await next();
console.log("middleware1 after");
};

const middleware2 = async (ctx, next) => {
console.log("middleware2 before");
await next();
console.log("middleware2 after");
};

const middleware3 = async (ctx, next) => {
console.log("middleware3 before");
await next();
console.log("middleware3 after");
};

const core = async (ctx, next) => {
const { a, b } = ctx;
const result = await add(a, b);
console.log(result);
ctx.result = result;
};

const applyMiddlewares =
(...middlewares) =>
async (ctx) => {
const chain = middlewares.reduceRight(
(acc, curr) => () => curr(ctx, acc),
() => {}
);
await chain();
return ctx;
};

const ctx = await applyMiddlewares(
middleware1,
middleware2,
middleware3,
core
)({ a: 1, b: 1 });
console.log(ctx);