popmotion 设计篇2 :中间件与扩展

在上一篇中,我们设计出了这样一种动作类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Action{
constructor(props){
this.props=props;
}

start(executor){
let {
update=v=>{},
complete=()=>{},
error=()=>{}
}=executor;

let {do}=this.props;

do(update,complete,error);
}
}

function action(do){
return new Action({do,});
}

我们为Action在初始化的时候提供一个算法框架函数do(update,complete,error),其中以三个名为updatecompleteerror三个函数作为基础,构建了整个函数的执行逻辑。具体在启动动作的时候,再传递一个执行器executor即可。

不过,这种方案有个局限性在于,我们只能遵循do的算法框架。可否再在某种算法框架do的基础上,对之进行动态修改?

举个例子,算法框架中调用了三次update(x)

1
2
3
4
5
const a=action(({ update }) => {
update(1);
update(2);
update(1);
});

如果我们觉得这里调用次数过多,难道要重修必须要重写一个算法框架吗生成新的类吗?

当然不用,借助于装饰器的思想,我们完全可以提供一个filter(predicate: Predicate)方法,来决定是否真正调用start({update,})中注入的函数,语法和效果类似于:

1
2
a.filter((v) => v === 1)
.start(console.log); // 会输出 1, 1

基于装饰器的思想,我们完全可以updatecompleteerror函数进行装饰,从而在一定程度上修改算法框架、扩展算法框架。

比如,针对以上的filter(predicate)方法,可以轻松写出以下代码:

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
class Action{
constructor(props){
this.props=props;
}

filter(predicate){
let origin_do=this.props.do;
let new_update=(update)=>{
if(predicte(v)){
update(v);
}
};
this.newUpdate=new_update;
}

start(executor){
let {do}=this.props;

let {
update=v=>{},
complete=()=>{},
error=()=>{}
}=executor;

update=this.newUpdate?this.newUpdate:update;

do(update,complete,error);
}
}

很丑陋是不是?如果我们有其他类似的装饰器方法,怎么办?显然,我们需要一个数组,把类似filter(predicate)这种东西存起来,然后再在start(execute)调用do(update,complete,error)之前,动态合成出具体的updatecompleteerror

于是,每次调用filter(predicate)之类的方法,就相当于定义一种可以施加的影响:可以再将来把(updatecomplete,error)变换为新的(update,completeerror)。

为了方便,不妨把这种转换称之为middleware,于是我们可以写出以下实现:

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
50
51
52
53
54
55
56
class Action{
constructor(props){
this.props=props;
// this.props.do 是一个算法框架函数
// this.props.middlewares 是一个中间件数组
}

// 这里为了直观,我们直接修改当前对象,
// todo: 改为创建一个新对象,让代码更函数式
filter(predicate){

// 自定义一个用于转换的中间件
let mw=(update,complete,error)=>{
return {
"update": (v)=>{
if(predicate(v)) update(v);
},
"complete": complete,
"error": error
},
};

this.props.middlewares.push(mw);
}

/**
* 根据executor和自身的middlewares合成新的函数对象
*/
_produce(executor){
let {
update = (v)=>{},
complete =()=>{},
error=()=>{},
}=executor;

let middlewares=this.props.middlewares;

for(let i=0;i<middwares.length;i++){
let mw=this.middlewares[i];
let r=mw(update,complete,error);
update=r.update;
complete=r.complete;
error=r.error;
}

return {update,complete, error };
}

start(executor){
let {do}=this.props;

let {update,complete,error}=this._produce(executor);

do(update,complete,error);
}
}

至此,我们完成了大致的设计(简易版)。不过,这个方案目前仍至少有以下几个小缺点:

  1. 以上filter(predicate)这种实现的写法显然在改动自身,为了符合不可变的思想,我们可以把filter(predicate)设计成返回一个新的对象的方法,也就是说,某种程度上,算是一个工厂方法。
  2. 以上的思路还是有些过度设计。毕竟大多时候实现只要多updatecomplete进行扩展,而每次都中间件调用后都返回三个函数构成的对象有些太重了——我们更多时候只需要新的update而已,所以官方对middleware的定义不再是调用后返回新的{update,complete,error}对象,而是:
    1
    2
    3
    4
    type Middleware = 
    (update: Update, complete?: Complete)
    =>
    (v: any) => any

此外,我们的complete()调用并不会终止框架算法的执行,为了做到这一点,需要引入一个简单的标志变量或者观察者(Observer),这样在链式组合调用过程中,就可以通过complete表示是否要在某个时间点终止执行。官方是的实现方案是将具体如何执行算法框架也放到了观察者中。不过这种细节并不影响总体的方案设计。

至此,我们具备了这样一种能力,即:可以通过简单地自定义其他中间件的函数,以良好的扩展性设计出丰富的功能。如pipe(funcs)

1
2
3
4
5
6
7
8
9
10
11
12
const double = (v) => v * 2;
const px = (v) => v + 'px';

const one = action((update,complete,error)=>{
update(0.2);
update(2);
update(1);
});
const twoPx = one.pipe(double, px);

one.start(console.log); // 1
twoPx.start(console.log); // '2px'