大家好,今天我分享的主题是《前端数据层初探》。
这个题目看起来很大,我会试着结合一些例子,试图讲的具体一点。
首先部分同学肯定会有一个疑惑,什么叫数据层?其实大家最近更经常听到的应该是数据流。这些概念其实都比较抽象,我们先脱离前端领域,从实践经验比较丰富的服务端领域来尝试理解。
我选取了我比较熟悉的javaWeb。这是java应用比较常见的一种分层方案。我们大部分人应该都写过一些简单的web服务,我从具体场景来介绍一下这个分层方案。
假设前端发起了一个请求,传上来一个用户的id,要获取这个用户所属的家族的家族成员列表,这时候请求会先到达controller,控制器。
当然数据是存储在数据库的,那我们中间这一层一层的东西是什么?首先是service,由于控制器是直接面向前端的,通常会做一些数据格式的处理、字段筛选的逻辑,面向视图层。因此通常我们不会在控制器里编写具体的面向业务的逻辑。比如这个场景,我们就需要在service中查询用户表,获得用户的家族id,再到家族表中把家族成员的id给查询出来,整合成一个数据返回给controller,所以service也有命名为logic的,通常就是处理具体业务逻辑的层。
Model层,领域模型层是做什么的?这个我们先跳过,来看一下DAO这一层,也就是数据访问层。由于服务端对数据库的访问是比较繁琐的事情,通常还要做一些关系数据库到对象的映射,所以这一层是直接面向数据库的表结构编程的,很机械,比如根据id查询用户数据,根据id更新用户数据之类的逻辑。这一层无法对业务领域进行抽象,所以通常会讲面向某种业务领域的逻辑抽象成领域模型,也就是Model层。
如果区分不了DAO层和Model层,其实可以思考一下,比如我们抽取了一个UserModel,用来处理用户的逻辑,那可能会有一些数据库底层没有的逻辑,比如对用户名进行长度拦截、对用户名进行合法性校验等等业务逻辑。
所以一个请求的链路就完成了,controller获取请求参数、调用service,servies调用多个DAO,DAO查询数据库,然后实例化各种领域模型,处理完数据后返回给Controller层,最后返回一个响应。
因为数据库是天然的单一数据源,请求到响应这样一条链路,几乎没有额外逻辑。 我们通过合适的分层和抽象,就可以让代码非常清晰可维护,也不容易写出bug。
服务端的目的是实现数据业务,那前端呢?
前端不同的地方在于,前端的根本目的是实现人机交互,前端之所以有数据层这个概念,是因为前端还有视图层。
我们理解了什么是数据层了,也就是将业务模型、业务逻辑分层,单独放在一起。那么前端需要数据层吗?
首先这个问题不能一概而论,前端是一个很大的概念,任何实现人机交互的应用都可以称为前端应用。目前主流的类型,我大致可以归纳出这四种。展示性就是业界所说的H5,游戏,业务型是我们最常见的,比如数据分析平台这种实现某类业务的,功能性可以理解成比如 音乐播放器、视频播放器这类应用。
那什么类型的应用最需要数据层?
很容易想到,就是业务型的应用。自动互联网爆发,导致众多传统的业务都开始转型互联网以后,业务型应用的需求就开始激增,这也是前端的三大框架出现的契机,因为业务型应用非常适合数据驱动的方式来开发。
业务型应用开发有什么难点,导致我们需要单独抽取一层数据层?
当然也不是所有业务型应用都具备一样的难度,但是遇到的问题都是非常类似的。
第一点就是数据本身复杂了,通常我们会处理许多来源的数据,如服务端返回的业务数据、程序里配置的数据、为用户交互、表单而创建的数据、一些时间相关的逻辑,还要处理定时器返回的数据、DOM事件,比如路由变化这个最经典的。还有localStorage这些本地缓存、websocket这种实时数据等等。
第二点就是原始数据和展示数据通常具有差异性。
三是程序处理的时序控制,典型的就是竞态问题,比如你正在看一篇文章,但是加载太久了,于是你切换了另一篇。过了一会儿之前第一篇文章数据返回了,如果没有处理好,第一篇的数据可能会把正在看的第二篇文章覆盖掉,导致出bug。
四是数据缓存优化,一些很少变动的数据如果要做缓存,如何优雅的实现也是个可以深究的问题。
这些问题可以用一个简化的业务场景来思考。
有个博客管理后台,这是文章列表,列表上显示这文章的标题和一个删除按钮。删除按钮会根据登录用户的身份是否是管理员,来切换可用和禁用状态。点击删除时,会发起一个请求,删除按钮变为加载状态,删除完成后,从列表上移除这一项。
我们在处理这个业务时,通常做法会是下面这样的流程。
- 请求商品的数据和用户的信息
- 对返回的数据做一层处理,比如某些值的判空、默认值补充、增加一些业务状态对象,比如为商品增加一个属性 isDeletable,表明是否有权限删除该商品。
- 根据用户的信息,设置商品的isDeletable的值。
- 由于列表删除时有loading的需求,为每一个列表的数据增加一个isLoading状态,用于绑定删除按钮的loading状态。
- 用户点击删除,会请求删除接口,同时将该项的isLoading设置为true
- 根据返回值是否删除成功,处理列表数据,重置isLoading状态
这个处理流程,在数据驱动的背景下,应该是非常容易理解的。但是其实上面做的这些步骤,其实是在做很多不同的事情。
- 我们请求了业务数据,这里实际上对应的是后端的模型层。
- 对业务数据进行了清洗、聚合。所谓清洗,就是讲其中我们不需要的数据去除、将一些边界情况较多的数据做一下兼容处理,比如判空。然后将多个数据进行了聚合,通过用户信息,计算出了商品是否可删除这个业务状态。这个时候已经开始做比后端返回的模型更多的事了。
- 针对交互进行建模。根据交互的复杂度,这一步可复杂可简单。我们的例子里就是为数据增加了一个是否正在loading的状态,这一步其实本质上实在针对交互进行建模。
- 接收用户交互,这其实是前端应用的本质工作,而所作的针对交互建模,也是为了接受用户交互的。接下来会更新交互模型,请求业务数据。
- 最后我们根据业务逻辑,更新本地的业务数据和交互数据,更新视图层。
这个过程在我们开发这类应用中,通常会重复几十上百次。依赖于目前流行的vue、react、angular这些框架,以及配套的数据仓库方案,我们通常可以驾轻就熟的完成这个简单的逻辑。
我们可以参考以网上一张MVVM示例图,来思考这个过程。
vm对象和UI进行声明式绑定,VM通过ajax和后端的model进行通信。VM在这里,其实要做的事情有很多。几乎我们列出的这几个步骤,都是通过VM对象打通的。而这个图中有一个过于理想化的地方,就是model。这个model,它是model吗?根据我们刚才的步骤,前端的model与后端的model之间,至少还差了一层数据清洗和聚合、针对交互进行建模的动作。
即使使用了vuex、redux这类状态管理库,其实他们本质上只是将组件局部的状态提升到了全局罢了,并没有解决以上这几个步骤,这就会导致一些很有趣的现象。有的同学会将store作为model,在action中,做完所有数据清洗和聚合的工作,放到state中的是最终的交互模型对象。这个是做的比较好的行为。而有同学则处于懵懂状态,有时是按照刚才的做法,有时是将store作为原始数据,在组件内取值时,才开始进行数据清洗、聚合的工作。
这是为什么?因为没有范式的指导。即使三大框架都可以很容易的实现我所说的模型层、逻辑层,但是并没有给出一个范式的指导。当然这里面可能是为了考虑保持框架的灵活性,没有去做这么一个限制。
那么我们应该用一个什么方式,才能将以上几个逻辑分层,从而让我们的应用更容易维护和拓展?
不必进行什么颠覆 或者编写什么框架,其实只需要在当前的状态管理和数据驱动之前,加上一层数据层。
数据层中 分为模型层和服务层,服务层负责请求数据,返回业务数据。模型层用来结合业务和交互进行建模。在数据层中,对数据进行清洗、组装、建模后,将模型交给状态管理器。当用户产生交互时,将操作提交到逻辑层,根据业务需要来控制应用状态,提交服务。
基于这样的分层,我们可以做一些更复杂的事,比如对业务透明的数据缓存,和对业务透明的竞态更新之类的复杂逻辑,可以统一在数据层机型一些处理。
接下来我们来回顾一下我们这个分享的目的,首先是我们进行这一系列探索的目的。首先是回顾和总结以往进行业务型应用的开发时,我们常规的做法和一些问题。
然后通过分析,让我们提升大局观,对我们正在编写的逻辑的本质心中有数。
这也为我们后续开发时提供一个方法论,寻找应用的结构中可能存在的优化点,甚至是为重构提供一个方向。
最后总结一下这堂课的内容。
首先是分析了前端应用的数据处理的复杂现状。
然后通过一些案例,分析出当前主流的前端框架在数据层上的缺失。
最好我们给出了一个有落地可能的开发范式。