关于前端数据&逻辑的思索

近来重构了一个项目,一个基于redux模子的react-native项目,目的是在杂沓的代码中梳理出一个清楚的构造来,为了完成这个目的,起首须要对项目的构造做分层处置惩罚,将各个逻辑星散出来,这里我是基于典范的MVC模子,那末为了将现有代码重构为抱负的模子,我须要做以下几步:

  • 拆分组件
  • 逻辑处置惩罚
  • 笼统、聚合数据

组件化

这是一个陈词滥调的题目了,从16年起前端除了构建东西,议论的最多的就是组件化了,把视图根据一定划定规矩切分为多少模块历程就是组件化,那末组件化的重点就是谁人划定规矩

那末这个划定规矩又是什么呢?

按功用?按款式?

我之前的项目里多半这两种状况都存在,举个简朴的例子,关于app的登录模块来讲就是一个典范的按功用分组,而关于一个列表就是一个显著的按款式去组件化,他们两个对应着两种完全差别的写法,因为他们一个是充血模子,一个是血虚模子。在redux中,显著的区别是血虚组件中统统的状况悉数外置,组件本身不去治理本身的状况,统统放到reducer;而在充血组件中,一部份状况由全局的store去治理,一部份有本身的state掌握。

    // 充血组件              // 血虚组件
    组件A | 组件B | 组件C    组件A | 组件B | 组件C
    逻辑A | 逻辑B | 逻辑C    ---------------------
    数据A | 数据B | 数据C           逻辑层
    -------------------     ---------------------
          全局逻辑                  数据层

在我重构的历程当中更倾向于将组件内的状况都放在reducer中,如许View就能够更地道的去衬着了,如许的View在我看来会越发简约、越发清楚,关于组件的替代更是轻车熟路。但状况全外置这类实践带来的价值也是很大的。因为一个带交互的组件,必将须要一些事宜的处置惩罚,生命周期的触发等等操纵,这会带来一些题目:

  • 这类组件提炼出来的状况只和本身有关,强迫被放在Store中就会带来Store复杂度的上升,假如你的组件充足多,那末全局的Store会膨胀的迥殊显著,更主要的是假如你的状况是和组件成树形对应的话,Store中将会冗余许多反复的数据。
  • 形貌组件的状况被转移到外部,致使操纵组件的本钱变高,关于组件内的一些简朴操纵将变得复杂烦琐。

关于后一点我以为并没有很大的题目,得益于分层和纯衬着的设想,组件将掌握本身的行动交出后能够将这些逻辑笼统为越发通用的逻辑,从而轻易有类似需求的组件运用,因为逻辑应当只出如今一个处所,而不该疏散在多个处所。比方掌握一批组件的显现或隐蔽,将组件内部掌握显现的逻辑交出来反而会省去更多的反复代码。

而我更忧郁的是因为组件中私有状况的转移致使的Store膨胀的题目,为了防止这个题目起首做的就是尽量的提取公用有类似作用的状况,比方掌握显现/隐蔽、多个列表的页数/条数;等这些有着类似功用的字段。走到这一步就引出了别的一个题目了,关于组件的状况形貌是树形的照样平行的。

  • 树形构造

这类构造的特点是将一个组件的状况经由过程一个树的情势纪录下来,页面是怎样嵌套的,那末状况树就是怎样嵌套的,如许做的优点是组件接收到状况后直接递归的显现就好了,关于组件来讲这是最简朴,效力最高的展现情势。但如许做的题目就是假如有多个类似的组件就会形成Store中冗余大批反复数据,终究形成Store的膨胀。

  • 平行构造

这类构造和上面的树形构造恰恰相反,能够最大水平的防止冗余数据的发生,将每一类数据拍平保留,但这类情势关于组件的展现却很不友好,组件须要本身去消化多处数据源带来的格式化操纵,在redux中connect要领就是用来处置惩罚这类多半据源聚合用的。

那末上面两种构造改怎样弃取呢?我个人引荐第二种平行构造,既然挑选了平行构造,那末该怎样行止置惩罚数据聚合的题目呢?在这里我引荐运用管道的思绪来处理,这自创了 Angular 2 Pipe的观点,固然熟习Linux的同砚关于 | 操纵符一定也不会生疏。在我们的项目中,数据是活动的,犹如一个管道中的水一样,Store就是一个水库,汇集了林林总总的数据(水),而页面组件就犹如须要浇灌的田,而从水库到田间这段间隔就须要水管的协助了。一样的,运用pipe我们能够将保留在Store中的数据转换成希冀看到的构造,而这统统操纵都是在数据的活动中完成的,而不是放在数据已通报到组件以后行止置惩罚了。

这里引出了一个观点,就是数据流这个观点,在项目中我将一切数据的操纵都成为数据的活动。举个例子,当用户在登录框输入了用户名和暗码并点击提交以后,这两个input中的value就变成了两个数据流:

   input => merge(name, password) => filter(校验合法性) => post(效劳器)

这个行动变成了一条流水线,先不论post输出的效果怎样,在上面的demo中我们的输入行动被笼统成了两个参数,末了经由过程兼并、过滤、发送,终究抵达效劳器,这不是一个新观点,在许多的框架中都有表现:

在Cycle.js它被称为 Intent(担任从外部的输入中,提掏出所需信息),Intent实际上做的是action实行历程的高等笼统,提取了必要的信息。因为View是纯展现的,所以包含事宜监听在内的行动统统被Intent笼统成数据源,这在RxJs中很罕见:

var clicks = Rx.Observable.fromEvent(document, 'click');
clicks.subscribe(x => console.log(x));

// 效果:
// 每次点击 document 时,都邑在掌握台上输出 MouseEvent 。

比拟于从View中发出的同步数据源,我们碰到更多的是从HTTP中猎取的异步数据源。在redux中我们经常使用redux-thunk来处置惩罚异步操纵,那末在流中呢?

逻辑处置惩罚

在之前的营业中我们有许多体式格局行止置惩罚异步操纵,比如说最经常使用的redux-thunk(回调)、promise、async/await。如今许多人更情愿用async/await操纵符去写异步逻辑,因为它让代码显得越发“同步”,我之前也很喜欢这类体式格局,但如今在数据流的观点中,同步/异步已被“隐约”了,它们都是数据源,它们都是“主动”发出数据的,那末同步照样异步就显得不那末主要了,照样上面的例子,假如用户名变成了一个异步猎取的历程,而不是用户主动输入的了:

 input => merge(async(name), password) => filter(校验合法性) => post(效劳器)

这类状况下在RxJs中能够经由过程zip来守候悉数的数据流

let age$ = Observable.of<number>(27, 25, 29);
let name$ = Observable.of<string>('Foo', 'Bar', 'Beer');
let isDev$ = Observable.of<boolean>(true, true, false);

Observable
    .zip(age$,
         name$,
         isDev$,
         (age: number, name: string, isDev: boolean) => ({ age, name, isDev }))
    .subscribe(x => console.log(x));

// 输出:
// { age: 27, name: 'Foo', isDev: true }
// { age: 25, name: 'Bar', isDev: true }
// { age: 29, name: 'Beer', isDev: false }

经由过程如许的链式操纵,我们能够很轻易的掌握和猎取数据流,这是关于数据的猎取,那末数据的分发呢?在redux中,我们一般会屡次dispatch,在redux-thunk中我们会如许写:

const getInfo = (params) => async (dispatch, getState) => {

    // TODO...
    
    dispatch(actionaA);
    
    // TODO...
    
    dispatch(actionaA);
}

而在redux-observable中:

const somethingEpic = (action$, store) =>
  action$.ofType(SOMETHING)
    .switchMap(() =>
      ajax('/something')
        .do(() => store.dispatch({ type: SOMETHING_ELSE }))
        .map(response => ({ type: SUCCESS, response }))
    );

然则我以为随处dispatch是一个不好的行动,这会让一个流变得杂沓,因为你在流的末了不会得完全的效果(在历程当中有一部份就已派发出去了),这会让逻辑看起来很狼藉,所以我引荐应当写成如许的情势:

const somethingEpic = action$ =>
  action$.ofType(SOMETHING)
    .switchMap(() =>
      ajax('/something')
        .mergeMap(response => Observable.of(
          { type: SOMETHING_ELSE },
          { type: SUCCESS, response }
        ))
    );

// 上面这两段demo来着redux-observable的文档

完毕了异步的处置惩罚,我们的流模子也完成了input->output的完全闭环了。在这里没有详细说output是因为基于redux,我任然是经由过程redux的connect要领将Store分发注入到组件的props中去的,因而假如你熟习redux那末会很习气如今的转变。

在处置惩罚完了同步/异步以后我们就来聊聊营业的逻辑该怎样处置惩罚了。在redux中逻辑被分在了两个处所,action和reducer中,一个是做数据的聚合,一个是做数据的格式化。上面提到了Intent 是action的高阶笼统,实际上是对action的拆分,剥离了action中猎取数据的部份逻辑,那末剩下的就是数据处置惩罚的部份了,这部份在我的实践中被叫做Service

这是一个单例的实例,全部项目中一个效劳只会有一个实例,没必要将雷同的代码复制一遍又一遍,只须要建立一个单一的可复用的数据效劳,而且把它注入到须要它的那些组件中。而且运用零丁的效劳能够坚持组件充足的精简,同时也更轻易对组件举行单元测试。一样reducer中的数据格式化逻辑也迁到了效劳中行止置惩罚,在redux中reducer统筹着数据的格式化和数据的保留这两个功用,如今我们将完全剥离出数据的处置惩罚部份,剩下的reducer将只做数据的保留,这就又引出了另一个观点Model,这一层我们一会议论,接着营业处置惩罚来看,在数据流猎取到数据并处置惩罚分发到Model中以后,input这一步基础算是完毕了,接下来就是由Model到View的output了。

上文中我说道了我引荐运用平行形式,那末在平行形式到View这类树型构造该假如转化呢?这是output中最主要的一步,在CycleJS中这一步一般由filter去完成,而在Angular中则是由Pipe行止置惩罚,不管它叫什么,它们都是这条流程上的一环,就像水管中的一节一样,一切从Model通向View的数据都邑进过这一环,从而被格式化。在代码中我更引荐人人尝试运用Decorator去过滤数据源:

@UserInfoPipe({ name: 'Model.UserInfo.name' })
class LoginDemo extends Component {
  constructor(props) {
    super(props);
  }

  render(){
    return (
      <View>
        <Text>{this.props.name}</Text>
      </View>
    );
  }
}

笼统、聚合数据

如今团体的骨架已有了,剩下的就是该怎样更好的笼统整合项目中的数据了。

  • 第一阶段

最一开始的项目因为为了轻易,我就根据API的构造去设想Store,谁人时刻一个页面对应一个接口或许很少的几个接口,这时刻我将API返回的构造与当地的状况一一对应,这在早期非常的轻易,不须要我做过量的转换,但是接下来为了敷衍接口的种种非常,不能不写许多防御性的代码(字段判空、属性变动、接口数据拼装),末了这些代码变得痴肥不堪,在别的同砚参与修正的时刻老是一头雾水,老是改了这里,那边出又出了题目。而且这个中也存在不少冗余的数据。

  • 第二阶段

厥后我发明既然数据都是终究给View去用的,那末我就按View的需求去设想Store好了,这个Store关于展现的组件来讲,运用起来非常轻易,当前运用处于哪一种状况,就用对应状况的数组范例的数据衬着,不用做任何的中心数据转换。不过这也一样形成数据冗余的题目,而且假如我须要修改页面的某个字段的话,须要在许多处所去修正,因为这个Store树变得很深枝恭弘=叶 恭弘许多。

  • 第三阶段

那末我如今该怎样设想状况呢?作为一个曾做过一段时间后端的我来讲,我决议模拟数据库的构造去设想状况树。把Store当做一个数据库,每一个品种的状况看作数据库中的一张表,状况中的每一个字段对应表的一个字段。

那末设想一个数据库,应当要遵照哪些准绳呢?

  • 数据根据域分类,存在差别的表中,每张表存储的字段不反复
  • 每张表中每条数据都有一个唯一主键
  • 表中除了主键外别的列,互相不存在依靠关联

而基于上面这三条准绳,我们怎样设想Store呢?

  • 把全部项目根据一定模子去星散为多少子状况,这些子状况之间不存在反复冗余的数据。

怎样明白这件事呢?举个例子,我有一个长列表,每当我点击列表中的某一列时就会有一个红框涌现包裹住这列,而这个列表中真正展现的数据应当是别的一个子状况,它们的关联类似:

{
    activeLine: 1,
    list: [
        {
            name: 'test1',
        },
        {
            name: 'test2',
        },
        {
            name: 'test3',
        },
        {
            name: 'test4',
        },
    ]
}
  • 以键值对的构造存储数据,用key/ID作为纪录的索引,纪录中的其他字段都依靠于索引。

有了唯一的key做主键,我们就能够很轻易的去遍历/处置惩罚数据。更进一步的,假如我们想去推断一条数据有无变化,我们能够纯真的去推断主键是不是一致,在一些状况下,这是一个不错的思绪,这防止了多层推断,或许深拷贝带来的复杂度和机能题目(这个能够参考immutable)。

  • 状况树中不保留能够经由过程已有数据盘算出来的数据,也就是这些数据都是互相自力的,都能够被称为原子数据

什么是原子数据?页面中运用到的数据都是由这些原子数据经由过程盘算、拼装获得的(注重:这里只要拼装,没有拆分,因为原子是最小的单元,所以是不可拆分的);这就坚持了数据源的一致,不会涌现一份一样的数据来自多出数据源的题目了,这会防止许多没必要要的题目,如多处数据源差别步致使的页面展现非常等题目。

好了,数据层也设想完了,如许一个完全的构造就清楚的摆在面前了,终究总结一下这个历程:

  • 根据血虚模子星散组件
  • 经由过程定阅的情势收集数据源
  • 经由过程数据库的情势去保留数据
  • 经由过程流的体式格局行止置惩罚和分发数据
  • 经由过程流的情势去格式化数据

经由以上几步,我们就开端的完成了一个营业从input到output的完全闭环。

已上这些就是我此次重构总结的一些履历,一定不全对、不完善、不正确,然则这个慷慨向我以为是值得去探究的。

    原文作者:jonnyF
    原文地址: https://segmentfault.com/a/1190000013279131
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞