前置条件
听说过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);
控制台日志:
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);