关于前端数据&逻辑的思考
最近重构了一个项目,一个基于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(服务器)
51220网站目录 https://www.51220.cn
这种情况下在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的完整闭环。
已上这些便是我这次重构总结的一些经验,肯定不全对、不完善、不准确,但是这个大方向我觉得是值得去探索的。
关于前端数据&逻辑的思考的更多相关文章
- 我的前端故事----关于前端数据&逻辑的思考
最近重构了一个项目,一个基于redux模型的react-native项目,目标是在混乱的代码中梳理出一个清晰的结构来,为了实现这个目标,首先需要对项目的结构做分层处理,将各个逻辑分离出来,这里我是基于 ...
- Javascript模板及其中的数据逻辑分离思想(MVC)
#Javascript模板及其中的数据逻辑分离思想 ##需求描述 项目数据库的题目表描述了70-120道题目,并且是会变化的,要根据数据库中的数据描述,比如,选择还是填空题,是不是重点题,题目总分是多 ...
- Webservice WCF WebApi 前端数据可视化 前端数据可视化 C# asp.net PhoneGap html5 C# Where 网站分布式开发简介 EntityFramework Core依赖注入上下文方式不同造成内存泄漏了解一下? SQL Server之深入理解STUFF 你必须知道的EntityFramework 6.x和EntityFramework Cor
Webservice WCF WebApi 注明:改编加组合 在.net平台下,有大量的技术让你创建一个HTTP服务,像Web Service,WCF,现在又出了Web API.在.net平台下, ...
- 对WEB前端的几段思考(一)——界面设计和性能优化(整理中)
尽管我并非艺术出生,既没有任何设计基础,又没有较高艺术涵养,也深谙在短时间内创造一定艺术造诣并非易事,但是既然当初选择从事网站前端开发,我的目光不能仅停留在前端代码上.作为一名志向在前端领域发展的人员 ...
- Handlebars的基本用法 Handlebars.js使用介绍 http://handlebarsjs.com/ Handlebars.js 模板引擎 javascript/jquery模板引擎——Handlebars初体验 handlebars.js 入门(1) 作为一名前端的你,必须掌握的模板引擎:Handlebars 前端数据模板handlebars与jquery整
Handlebars的基本用法 使用Handlebars,你可以轻松创建语义化模板,Mustache模板和Handlebars是兼容的,所以你可以将Mustache导入Handlebars以使用 Ha ...
- 3年磨一剑,我的前端数据 mock 库 http-mock-middleware
不好意思,离开博客园4年多了,一回来就是为自己打广告,真是害羞啊... http-mock-middleware 是我最近完成的一个前端数据 mock 库.它是我汇总近3年工作经验而诞生的一个工具,使 ...
- Oracle数据逻辑迁移综合实战篇
本文适合迁移大量表和数据的复杂需求. 如果你的需求只是简单的迁移少量表,可直接参考这两篇文章即可完成需求: Oracle简单常用的数据泵导出导入(expdp/impdp)命令举例(上) Oracle简 ...
- 开源来自百度商业前端数据可视化团队的超漂亮动态图表--ECharts
开源来自百度商业前端数据可视化团队的超漂亮动态图表--ECharts 本人项目中最近有需要图表的地方,偶然发现一款超级漂亮的动态图标js图表控件,分享给大家,觉得好用的就看一下.更多更漂亮的演示大家可 ...
- 设计一种前端数据延迟加载的jQuery插件(2)
背景 最近看到很多网站都运用到了一种前端数据延迟加载技术,包括淘宝,新浪网等等,这样做的目的可以使得一些未显示的图片随 着滚动条的滚动进行延迟显示. 好处显而易见,可以减少前端对于图片的Http请求, ...
随机推荐
- Java实现第八届蓝桥杯取数位
取数位 求1个整数的第k位数字有很多种方法. 以下的方法就是一种. 还有一个答案:f(x/10,k--) public class Main { static int len(int x){ // 返 ...
- 关于linux免密登录及ssh客户端的使用
操作系统环境: CentOS Linux release 7.7.1908 (Core) 1.首先在linux服务器上,使用ssh-keygen命令生成密钥对文件(一直回车即可,默认使用rsa算法), ...
- python3 基本书写规范
一.缩进在类.函数定义完成后需要接着写子代码快需要在定义完成后加上: 缩进格式为首字母开始空格4个位置(取消了大括号以冒号代替子模块)例: class pop: #类的定义方式 def pip: #函 ...
- Netty源码学习系列之1-netty的串行无锁化
前言 最近趁着跟老东家提离职之后.到新公司报道之前的这段空闲时期,着力研究了一番netty框架,对其有了一些浅薄的认识,后续的几篇文章会以netty为主,将近期所学记录一二,也争取能帮未对netty有 ...
- Zabbix+Orabbix监控oracle数据库表空间
Orabbix 是设计用来为 zabbix 监控 Oracle 数据库的插件,它提供多层次的监控,包括可用性和服务器性能指标. 它提供了从众多 oracle 实例采集数据的有效机制,进而提供此信息的监 ...
- ReentrantLock原理分析
一 UML类图 通过类图ReentrantLock是同步锁,同一时间只能有一个线程获取到锁,其他获取该锁的线程会被阻塞而被放入AQS阻塞队列中.ReentrantLock类继承Lock接口:内部抽象类 ...
- Java并发相关知识点梳理和研究
1. 知识点思维导图 (图比较大,可以右键在新窗口打开) 2. 经典的wait()/notify()/notifyAll()实现生产者/消费者编程范式深入分析 & synchronized 注 ...
- 如何在宝塔上的Nginx实现负载均衡
创建一个指向服务器本身的localhost站点(127.0.0.1)和一个指向服务器的站点,域名和IP都可以. I.对域名站点配置: upstream myproj { server 127.0.0 ...
- hql 转 sql
import org.hibernate.engine.SessionFactoryImplementor; import org.hibernate.hql.ast.QueryTranslatorI ...
- 13.Django-分页
使用Django实现分页器功能 要使用Django实现分页器,必须从Django中导入Paginator模块 from django.core.paginator import Paginator 假 ...