Flux用过了,Redux也用过了,仍是以为不顺手?要不要本身造一个?

1 前言

不少同窗用过了Flux,也用过了Redux,但仍是以为不趁心?要不要本身造一个?一百行来代码就基本搞定,So easy, so good!git

其实,本身造的框架实不实用,并不重要,重要的是思想。有了设计框架的思想后,再去看人家的框架,就会更多地关注人家为什么要这么设计?好处在哪?弊端在哪?是否有改进的地方?明白了框架设计者的想法,才能更好地使用框架。github

如今,我们就一块儿来设计一个React框架,这个框架具有如下几个的特色:浏览器

  1. 单向数据流:业务数据从UI层触发,经处理到Module层便结束,再也不须要人为地将数据反映到UI层。mvc

  2. 消息机制:组件与服务之间经过消息总线完成,包括组件与组件之间的嵌套关系。框架

我们给这个框架起个响亮的名字——Rebus(React-Bus)。这里的Bus不是公交车的Bus,是计算机基础原理中“Bus”(总线)。很显然,我们要用“消息总线”这样的思想,实现ReactJs的单向数据流开发模式。一句话归纳我们的框架:Rebus是一个基于消息总线的,单向数据流的,ReactJs开发框架dom

这里是用Rebus写的一个TodoMVC实例:https://github.com/odebo/todomvc-rebus(看在个人代码写得如此粗糙的份上,大虾们赏颗星星鼓励鼓励下呗)。异步

clipboard.png

2 什么是单向数据流模式

什么是“单向数据流模式”?这个概念对不少人来讲可能有点陌生。下面是Facebook的Flux官网(http://facebook.github.io/flux/)提供的说明图:函数

clipboard.png

好像有点抽象?那我们先补补脑,看看什么是双向数据流模式。工具

什么是双向数据模式?简单地说就是UI层的一个操做通过UI层(View)、控制层(Control)、模式层(Model),作完增、删、改、查等处理后,还得反过来,手动地将增删改查后的数据反映到UI层上。这就双向数据流模式。单元测试

而Flux中所谓的单向数据流模式是指:UI层监听应用的“状态”,当一个操做(Action)通过Dispatch(分发器)、Store(状态容器),最后更新了“状态”,UI层自动根据“状态”的变化而更新界面。

这里的“状态”是指一个应用某个时刻的某个状态:好比左侧菜单栏展开与否——状态;导航中高亮项是谁——状态;用户是否登陆,用户是谁——状态;Table中多少个Item,分别是什么内容——状态。

简单地说,单向数据流就是单向绑定,UI层与状态绑定,当状态发生变化,UI层自动更新。

可能有的同窗会问,既然有AngularJs这样的双向绑定的MVVM模式,还搞什么单向绑定模式,听起来弱爆了。双向绑定确定比单向绑定高大上得多。

这个问题不太好下结论,双向绑定当然有双向绑定的好处,但也有它的弊端。而相比单向数据流的逻辑处理思路更加单纯清晰。

3 Rebus 框架的数据流模型

理解了单向数据流后,我们给出Rebus框架的数据流模式(以下图)。归纳起来就三个步骤:

  1. UI层触发的一个Action。

  2. Rebus总线根据Action路由表选择对应的Service进行处理。

  3. Service处理后,更新状态(State),结束。

clipboard.png

这里的Services层指的是业务服务层,提供业务处理接口,包括对状态的修改,对后台数据的异步处理等等。若是以为这一层太厚,能够分离出专门的Modle接口层。但无论怎样,一个业务操做从UI层到最后修改状态便结束,数据流方向只有一个。

但光这么说仍是太抽象了,我们直接上代码,看看在TodoMVC这个例子中,添加一个新的Todo这个操做是怎么被处理的。

clipboard.png

clipboard.png

clipboard.png

是否是挺简单,简洁的三层结构,清晰的数据流:

  1. ReactJs组件只负责渲染和触发Action,具体谁来响应Action,它无论。

  2. Rebus总线根据Action路由表,调用对应的Service进行处理。

  3. Service层进行完逻辑处理后,经过Rebus.setState()方法更新状态。

但你必定会问:React组件是怎么监听状态的变化的?其实很简单,直接看代码:好比我们但愿添加新的Todo后,TodoBody组件会自动更新。因此TodoBody组件应该监听状态“todos”的变化。

clipboard.png

4 Rebus中的action

用过Flux的同窗都知道Flux中有个叫Dispatch的模块,用来dispatch各类Action。而我们的Rebus.execute()的做用与Dispatch.dispatch()差很少(以下图)。

clipboard.png

clipboard.png

不同的是Rebus.execute(actionHead, arg1,arg2,…)的第一个参数是action头,其它参数直接跟在action头后面。Action头中包含两个信息:要作什么?从哪里来?

“从哪里来”这个参数很重要,由于它给我们开发、调试提供了极大的便利。试想下,在Action路由表中,我们可以很清晰地看出一个Action将会到哪一个Service处理,但无法直观地看出一个Action是从哪里触发的,并且一样的Action可能由不一样的组件触发,这是无法从Action路由表中直观看出来的。

因此,我们给Rebus增长了一个调试功能,只要打开这个功能,即可以打印Action信息。

clipboard.png

clipboard.png

另外,若是一个Action被触发,却没在路由表中找到这个Action的路由,Rebus会经过打印错误信息的方式提醒开发者。

clipboard.png

自从Action有了源信息,领导不再用担忧我找不到代码的出处了,欧耶!

5 Rebus中的Action路由表

Action路由表这个概念在Flux与Redux中没有,但也很好理解,就是一个很直观的路由配置信息表。它是在Web应用开始初始化时,加载进来的。

clipboard.png

在这张Action路由表中,你能够直观地添加、修改、跟踪一个Action会被哪一个Service处理。当你但愿某个Action被另外一个Service处理时,直接在这个Action路由表中进行修改即是。

clipboard.png

另外,在这个Action路由表中,我们能够经过and()让一个Action触发多个service,如上图的第29行。我们写了一个日志服务TodoLog.logAddTodo,但愿系统处理ADD_TODO的同时也记录这个事件。我们就能够经过and()函数将这个服务绑定到ADD_TODO这条路由后面,and()的参数是一个数据,意思能够绑定多个服务。

可是,必须提醒的是,不建议and()中的服务也修改State,除非你确定and()中的服务修改的State与Rebus.connet()中的服务修改的State的监听者没有任何交集。因此,再三提醒and()中只绑定跟State无关的服务,好比一些日志服务、系通通计服务等。

可能你会问,一个Web应用就一张Action路由表吗?是的,也许在后续的版本中我们能够支持多个Action路由表。但一张路由表也有它的好处——惟一性。好比你设置了某个Action的路由,结果另外一个同事在另外一张路由表中也设置了同名的Action路由,一开始独立开发时可能没有问题,一旦整合在一块儿,问题就出现了。因此,只有一张路由表是有好处的,大点不要紧。

6 Rebus中的组件

我们都知道ReactJs的一大特征就是支持JSX语法,这使得JS代码中能够直接写“类标签代码”,并且一个组件可以被嵌套在另外一个组件中,并接受从上级组件传递进来的参数。

这种一层一层嵌套的写法虽然很直观,但也很蛋疼。就拿上面Redux实现的Header组件添加新Todo这个操做,执行的是传递进来的回调函数addTodo(…)。

clipboard.png

这么作有几个问题:

1)写代码时,究竟是先约定Header组件要执行的回调函数叫addTodo,写上级组件时按约定传递叫addTodo的参数?仍是先写好上级组件,根据上级组件传递的参数名来执行回调函数?究竟是先有蛋仍是先有鸡?

2)果上级组件传参时传错了,或者子组件写回调函数时名称写错了,如何跟踪代码,只知道光从代码上看,我TM怎么知道这个回调函数是从哪一个组件传进来的?虽然如今有些工具可以直接在浏览器上查看组件之间的嵌套关系,但那也是在应用可以正常跑起来的状况才能Debug。

3)组件与组件之间的关系是经过硬编码实现,若是如今有个子组件须要替换,但是这个子组件被嵌入在多个组件中,试问这得怎么找?

组件嵌套是ReactJs的一大亮点,但也是不少人认为ReactJs不适合作大型项目的缘由。但我以为这并非ReactJs的问题,咱们彻底能够其余途径解决上面这些问题。好比我们的Rebus,组件与组件之间不会直接嵌套,而是跟调用后台Service同样,经过Rebus.execute()方法,发起一个Action。好比TodoApp这个上层组件,它嵌套了TodoHead/TodoBody/TodoFoot这三个子组件,但你会发现TodoApp组件是经过execute了三个分别叫GET_TODOHEAD、GET_TODOBODY、GET_TODOFOOT的Action来引入三个子组件,具体引入是怎么的组件,它并不关心。

clipboard.png

Rebus总线根据Action路由表(rebus.route.js),分别找到这三个Action对应实现者(在这里我们经过一个“组件工厂”CompFactory来响应这些Action)。当咱们须要替换组件时,只须要在Action路由表中作出修改即是。

clipboard.png

换句话说,在Rebus总线面前,每一个组件都是平等的。组件只会跟Rebus总线沟通,不会直接嵌入其它组件,也不会被嵌到其它组件中。“组件树”这个概念在Rebus是经过Action消息来实现的,是一种“动态嵌套”关系。

7 Rebus中的State

在Flux/Redux中,应用的各类状态以一棵“状态树”的形式都是从根组件上灌进去,全部子组件的状态一概从这个根组件上继承下来(无论组件树的结构有多深)。这样作的好处就是一旦某个状态发生变化,React组件自动从上到下进行更新。

可是,这么作真的好吗?并非说一个应用就一棵状态树这个想法很差,我也赞同这种设计,由于状态是Web应用中最重要但又很是容易混乱的信息,“惟一性”对状态来讲,很是重要。

但是若是全部子组件的状态都是从根组件一层一层传递进来的话,至少会有两个问题:

  1. 组件之间的耦合性高,难以并行开发:子组件的状态是由父组件决定。那到底先写父组件仍是先写子组件?

  2. 状态变化后,难以跟踪变化的组件:假设你的某个操做修改了某个状态,但这个状态的变化会致使哪些组件更新了?光从Store中是看不出的,也没法跟踪,只能从根组件一层一层往下查,看看这个State被传递到哪一个组件中。

在Rebus中,我们一样维系着一棵“状态树”,并在应用初始化的时就加载进来的。

clipboard.png

但不一样的是,组件的状态不是从上级组件中传递进来,是经过Rebus得到的,并且组件有权决定本身关心哪一个State的变化。

clipboard.png

这样作有几个好处:

  1. 方便并行开发:由于组件之间没有太过的耦合性。状态都是经过Rebus得到的,大部分状况下都是直接返回状态树中的某个状态,这样的“浅处理”很是适用于复杂系统开发中。

  2. 方便单元测试:因为组件直接与状态绑定(监听),要对一个组件进行单元测试,直接修改这个组件绑定的状态即是,便是没有上级组件的存在,也不影响测试。

  3. 方便维护代码: 从上面的代码中能够清晰地看出某个组件监听哪些状态,但反过来,某个状态被哪些组件监听了?从组件的代码中是无法直观看出来的。这个问题也不该该经过查阅代码的形式来解决,而应该经过我们的Rebus来解决。我们能够给Rebus增长一个方法,打印每个State的监听者。以下图:

clipboard.png

clipboard.png

如今我们既能够清晰地看出一个组件监听了哪些状态,也能看出一个状态被哪些组件监听。这为代码的调试与维护提供极大的方便。

另外,咱们能够轻松地打印出某个时刻的状态树或具体某个状态的值。

clipboard.png

clipboard.png

8 总结

先给有耐心看到这里的人鼓个掌……而后也给我本身鼓个掌……由于对于一个拖延症极度病患者来讲,用业余时间写这么一篇技术贴真心不容易。当我写这句话的时候,距离这个帖子的第一句话,整整隔了一个月!——大哥,你是一禅指敲键盘的吗?

言归正传,总结下我们这个Rebus框架的特色:

  1. 实现了单向数据流模式,逻辑层次结构浅,思路清晰。

  2. React组件职责单一,只负责渲染与响应交互。

  3. 以路由表的形式控制Action数据的流向,直观、易维护。

  4. React组件之间经过消息的形式实现动态嵌套。