背景
目前公司内主要项目采用的状态管理 是Redux,有几十个reducer分片,reducer分片多的上千行少的也有百来行,业务状态多且杂,转化为视图状态的过程繁琐,且大部分场景难易复用逻辑 。
PC端和移动端的 hr hm 的业务大量重复逻辑,一个需求往往需要多次开发 。
项目内大部分组件有需要的情况下都是 connect的方式直接连接 redux获取数据。
困境
在使用redux的初期我总感觉不舒服,但是又说不出到底哪里不舒服,整理之后我找到了问题所在:
视图状态和业务状态耦合
对于视图组件来说我只需要一个meetingRoomOptions 用于渲染会议室项,但是被迫需要在视图内做一些和视图无关的逻辑判断,先是enabledMeetingRoomVendor 判断使用三方会议室还是系统会议室,然后再是根据locationId 去做一个排序,这些都是视图组件不关心的。
与此同时如果我再开发一次移动端的相同需求,这段状态处理的逻辑我可能就需要再次copy一次,同时将来维护的时候也需要多地维护,接盘侠也会在大量不明所以的逻辑判断中迷失。
项目中有大量的开关状态,用于判断特殊的业务场景,这些状态都存储在redux中。如果是控制不同组件的展示是没有太大问题的,但往往是需要复用相同组件,其中有部分区别,导致需要在视图代码中加入大量判断逻辑,用于处理业务状态转化为视图的过程 ,同时需要非常小心的在每个用到的地方进行处理,任何疏漏都会带来bug,最后开发完成之后的组件也往往惨不忍睹,视图无关的逻辑代码遍布各地,代码可读性差难易复用几乎不可测试 ,同时因为逻辑复杂,不熟悉代码的情况下开发很容易导致一些意料之外的bug。
作用域污染
在开发函数组件的时候,因为需要直接引入dispatch相关函数(此处为updateOrgMeetingRoom),污染了文件的作用域,导致在组件内部无法解构props,不仅是Eslint规则配置的原因,若解构出同名函数在后期改动的情况下也非常容易出现bug。
啰嗦
极度啰嗦,增加一个状态需要多个文件书写并且类型支持较差,阅读一个状态的变更流程也需要阅读多个文件。
耦合度高,调用状态不受约束
和项目代码耦合程度高,调用redux状态不受约束,隐式调用等难以查找和清除 。
侵入式
因为使用connect侵入式的方式给组件注入props ,导致想在后期再更换 状态管理框架是一件成本巨大的事情,比如在我吐槽redux 难用之后大家也只能无奈的说
历史遗留问题,没有办法
不受约束
都是为了获取feedbackTemplateEntities 一千个人有一千种获取的方式,后期想要将某个redux中的字段移除的时候,排查将是一种灾难。
性能
mapStatetoProps的设计无法过滤掉无用的通知,每当store变化时都会通知到订阅的组件,导致mapStatetoProps会多次执行,若是较轻的状态映射对性能没什么影响。
但目前项目几乎所有需要的地方都是直接connect 连接store ,且对业务状态的处理大量重复,并且mapStatetoProps中的耗时计算也将多次执行 。同时这里进行的是浅比较,而我们往往从store中取的状态都是引用类型,mapStatetoProps 每次计算都是一个全新的引用地址,导致组件多次render 。
不过目前来看性能不是问题? 也可能是现在电脑性能都不错。
探索
明确需求
-
最坏的味道就是判断逻辑和转化逻辑与视图混合在一起,我需要解耦,方便复用、测试和维护。
-
作用域污染容易产生隐式bug,同时挑战人体极限的体操式获取状态是不能被容忍的,需要约束调用并且做隔离。
-
mapStatetoProps造成的多次重复计算,其中耗时计算的问题可以通过reselect这个库来解决,但是最佳实践还是按需调用,我只对我感兴趣的状态更新,同时耗时计算我想要缓存。 -
开发起来啰嗦是现实,理想的乌托邦是希望可以傻瓜式增加状态。
为什么我在之前公司的开发感知不到状态管理的存在?
没有比较就感受不到差距。
在之前的公司使用的是 Angular进行开发,所有的状态都是基于service去做管理的
service和组件之间依靠 interface 去做一个连接。同时搭配Angular自带的DI,实现了视图组件和service的解耦,这使得对service的复用和维护非常容易 ,实际上我们也是这样开发的:
-
视图和视图service(或者说是具体service)通过 interface连接
-
具体service通过interface和抽象的 service连接
-
视图只负责交互逻辑,状态交给具体的service管理,不关心怎么来的,只关心我要什么(interface)
以至于当时在搭框架的时候我放弃了第三方状态管理库,全权交由service与Angular强大的DI
状态应该如何流动
我理想中的前端状态流:
视图只对用户负责,用于渲染视图状态和响应用户交互
每个视图组件都有属于自己的具象service,具象service只对视图interface负责
服务之间的是由抽象到具象的结构,原则上是禁止高层次的服务调用低层次的服务
由抽象服务做io相关副作用,抹平差异等内容
在redux的基础上解决问题
如果要将项目完全从redux中剥离是不现实的,成本过于巨大,最理想的方式就是一点点的把redux和视图之间的联系剔除掉,将redux与视图解耦。
-
基于上面的思考,我决定在redux和视图中间插入service层,弃用
connect,继续沿用redux来管理业务状态,通过reducer service,视图service等层层解耦,提高复用性。 -
同时也为了将service于redux解耦,弃用redux提供的订阅逻辑,使用redux 中间件配合rxjs来实现发布订阅。(自己实现一个发布订阅也可以,但是我希望通过rxjs将事件,状态变更等都抽象为流)
-
通过di的方式解耦service
中间件逻辑如下:
将响应式store通过di的方式注入到项目中:
构建service:
消费service:
好处
-
不再被状态管理库绑架,彻底解耦,将来你想让redux滚蛋也不是不行
-
强迫思考视图状态和业务状态,解决了业务状态到视图状态的转化,提高代码质量
-
状态调用显示且受约束,朕给你的你才能要,朕不给的你不能抢
-
抹平不同情况下的差异,业务逻辑可以抽象出来,多平台使用,同时测试代码编写难度大大降低
-
在 react redux背景下,避免因为
mapStatetoProps导致的性能问题,减少render次数和 耗时计算,视图只会在订阅的状态变更之后才收到通知,自定义compare可以变相缓存耗时计算状态
成本
-
开发成本会上升,强迫思考业务状态和视图状态,需要编写service,但是长远收益更可观
-
被复用的service需要编写相关测试
-
暂时只找到wedi这个DI 库比较满足需求,可能还需要再找一个相对稳定非个人维护的DI库,或者团队自己维护
-
service需要量变才能展现威力,旧的组件需要重构
后续考虑
- 既然都抽离业务service了,可以考虑在不同端复用service
其他
今天和tl聊了一下,了解到项目里有抽象转化状态的一些函数放在一个文件里,不过用的人蛮少的,而且这种抽象方式,除了作者应该很难将方法同步给其他人。 而且也仅仅是把一些公共逻辑复用了,该不舒服的点还是不舒服。
今天想到,selector这类公共的方法,相比写出好维护的代码,公共的方法和逻辑如何同步给同事才是最困难的,很难避免你实现了一个方法,可能几天也可能几个月之后你的同事又在其他位置实现了一个功能相同的方法。
但是换个角度想,在开发过程中我们写UI的时候,第一时间并不是去如何写一个UI组件,而且先去找找组件库,在完善的组件库和设计规范下,很多界面完全不需要业务开发者去写样式。 是不是可以借鉴UI组件的思想?
在完善的状态service组织下,我们在写业务的时候不需要想着如何处理这些状态,第一时间想的是我在哪个服务里可以获得这个状态? 如果拿不到我是选择让service维护者 或者是我自己来给service添加逻辑? 又或者是属于视图的领域service,我可以添加一个和视图生存周期相同的service。
自然界的过程都是向着熵增加的方向进行的,即从有序到无序。
显然代码也是如此,在没有外力介入的情况下,项目会膨胀,混乱。 将混乱收集起来,收口到一个地方在花较多成本去维护是还不错的选择。
参考:
- github.com/wendellhu95… react 中的di实现参考了该文