前端领域的数据状态统一管理机制
前言:
在谈前端数据状态管理之前,让我们先来聊聊这些年前端技术框架的变更,以及每个技术时代背景下“数据”在页面中处于一个什么地位。大概可以分为以下几个阶段:
Web 起步
准确的说在这个刚出现 web 概念的时代,还没有前端的程序员,大多数软件公司也没有前端开发的岗位。系统页面直接就是 jsp、asp。里面可以包含复杂的业务逻辑,数据库访问操作也在其中。一个 java 开发就能从底层到页面一条龙搞定,逻辑想写哪就写哪。给人的感觉就是一个字“乱”。
这个时代没有前端概念,更没有前端数据。
单纯页面、朴素的数据流
随着 HTML、ECMAScript 规范相继发布,规范版本的持续更新,以及 AJAX 技术流行起来,前端页面由一个个 html 组成。通过 get 获取后端数据展示,post 提交反馈页面数据。数据就这样在前后端网络来传递,可能会把一些公共的,反复使用的数据存储在 cookie 或者 js 内存中。
这个时代前端页面还是比较单纯的,有了一些数据存储的思想。
MVC、UI 和数据分离
当业务越来越复杂,页面上的数据也越来越多,随处可见的 ajax 请求,维护数据的成本增高。MVC 的架构就诞生了,它把页面上 UI 的渲染逻辑(View)和数据的处理(Model)分离,再用一个控制器(Controller)做中转,像我司 Fish 框架就是一个典型的 MVC 架构实践者。
但是在我看来,这种架构设计只是让前端抢了后端的一些数据处理工作,页面上虽然对数据做了统一的管理,但是这些数据都还是无状态的,页面的改变需要重新请求数据,根据数据变化来操作 dom。
MVVM 数据驱动视图
随着系统要求越来越高,以操作 dom 的方式去更新界面的方式对页面性能消耗巨大,已经满足不了系统性能要求。数据驱动视图很好的解决了这个问题。
现在前端两大主流框架 react 和 vue,设计思路都是数据驱动视图,即把页面的状态用数据来表示,如:元素的隐藏和显示就是一个 true 和 false,数据的选中状态就是设置数据 active 为 true。我们把这种可以描绘页面状态的数据称为 state,当页面发生变化的时候,无须关心 dom 的变化,只需关心对应 state 的变化即可。state 映射到页面这个过程交给框架来处理。同时,为了解决性能上的问题,虚拟 dom 产生了。
一个数据驱动视图的过程:
数据变化,生成新的虚拟 domReact 需要主动调用 setState 方法,Vue 做了数据劫持,监听数据的变化
diff 算法分析比对新旧两个虚拟 dom,得到差异(patch)
diff 算法核心就是递归遍历两个树状的数据结构,当前 React 和 Vue 两个框架都在性能方面做了很多的优化手段
根据 patch 更新 dom,也是我们通常说的打补丁
有了这个数据驱动视图之后,确实可以很大的提高页面的性能,但是这还只是针对单个组件的数据管理,组件和组件之间还需要有交互,需要有数据通信的,因此我们还需要有个应用级别的统一数据管理机制。
Flux 架构
系统业务更复杂了,一个页面被拆分了 n 个组件,组件的状态需要共享,一个数据状态需要在任何地方都可以拿到,一个事件需要改变全局状态或者其他组件状态。出现这些情况,如果不按照一定规律处理数据状态的读写,代码很快就会变成一团乱麻。这时需要一种机制,可以在同一个地方(store)管理数据,有统一的方法来查询状态、改变状态、传播状态的变化。这就是 Flux,利用单向数据流实现的应用架构。
Redux、Vuex、Mobx 都是 Flux 思想而出的状态管理库。虽然在实现方式上有些差异,但是思路都差不多。
本文以 Redux 为例子。
01 Redux 流程图
名称解释:
State:应用数据
Store:存储数据地方
Actions: 改变数据的指令
Reducers: 处理数据逻辑的
React Components:页面组件
全流程:
1、初始化 state,创建 actions
2、store 存在与 Store 中
3、state 和 actions 都传递给 Components
4、Components 根据需求发送一个 action 指令
5、Reducers 收到指令,处理得到一个新的 state
6、Components 接收到新的 state,更新页面
不用 Redux 和使用 Redux,组件数据通信的实现比较:
02 数据格式的设计
技术有了,就到实战了,能做到优雅的设计好的 state 数据,对于代码的观赏性和可维护性都有着至关重要的作用。对于 state 的结构设计,我总结起来需要遵守以下原则:
原则一:按照业务模块分块设计数据
大部分开发人员在设计数据结构的时候,比较容易犯的错误,有两个极端情况:
以接口的出入参数格式设计 state
一个接口可能需要同时支持 PC 和手机端多个渠道同时调用,不同渠道的页面原生展示有所差异,而且接口数据的层级可能很深,页面这边又需要扁平化展示,另外一个页面数据可能来自于多个接口。所以直接这样设计 state 不行。
举一个例子:
以页面表现设计 state
存在多个页面会共用一套数据,展示相同的内容的情况,这样以页面维度设计 state 也是不是合理的。
正确的设计思路是什么呢?应该从整体上考虑,整个系统拥有哪些要素,分别具备哪些属性,不受接口和 UI 界面的影响,按照要素去划分 state 的数据结构。
原则二:能用数据状态,就不要多套冗余数据
有了数据结构,我们还需要考虑它的状态,即有多少种取值,分别又代表什么状态。尽量的多用状态解决数据冗余。
举一个例子:页面上有一个 Tab 页,三个页签【“全部订单”,“最近办一个月”,“最近一年”】。三个页签的数据都来源于订单列表。
下面有两种 state 设计:
方案 1 用三套数据把不同页签的数据隔离了,切换起来会很快,但是数据冗余比较厉害,如果新增加一条一个月内订单数据,需要同时在三份数据里面都增加一条,维护起来特别麻烦,而且容易造成数据之间的差异。
方案 2 是我推荐的方案,虽然 flag 这个字段可能后端不会直接给到,但是我们只需在要在统一做一次计算就可以得出,后面使用起来就只需要维护这一套数据了。
03 数据状态的更新
数据状态也设计好了,对于数据状态的更新操作,我也总结了两个原则,遵守起来的话可以大大提高代码的可读性。
原则一:格式统一转换
上面也说过,不能直接用后端数据来设计 state,前后端数据格式存在差异是非常普遍的现场,作为前端,只需要守住数据出入口就可以:
入口:在取到数据的地方转换为我们设计好的格式,存入 state, 之后在其用到的所有地方数据格式都是这一套 。
出口:在需要提交数据到后端的时候,从 state 数据又转成接口需要的格式。
原则二:单例数据
之前也提到过,数据可以在组件内部自己维护,也可以全局统一管理,这两者并不矛盾,也可以共存。但是对于表达意思相同的数据只能存在一种。
先解释 React 对于组件的两个概念:受控组件和非受控组件。
受控组件:无自己 state,数据状态来源的参数,组件本身状态受调用者控制
非受控组件:有自己 state,数据状态自己控制,组件本身状态自己控制
对于一个复杂的页面,拆成的子组件,要么全部受控,要么全部不受控,行为保持一致。
原则三:唯一的 action
对于 store 里面的数据,其中任意一块,都只设计一个 action 来触发改变它,遵循一个单一原则如果设计多了出问题后不利于排查。
04 案例分享
分享一下“快速开发平台--页面编辑器”这个页面的数据设计方案。
背景
通过页面拖拽左侧的各种控件到中间操作区,左侧还有个层级表示中间操作区域的页面层级结构,选中区域上某个组件和左侧层级对象,右侧展示对应的属性。
state 设计思考
从系统全局考虑
考虑到控件的很多属性会存在相同,把所有的属性定义成一份,控件里面加一个属性列表的关联关系
考虑页面是用一个个控件排出来的,设计一个树形结构表示页面
考虑到点击选中控件效果,设计一个当前控件,表示控件状态
这样得出大致结构如下:
左侧的层级和中间的页面加载的是同一套数据 currPageInst,只是展示的粒度有差异。
05 结语
正所谓磨刀不误砍柴工,拿到一个复杂的页面需求,不妨停下来思考一下页面数据如何设计,在对数据的处理上遵守这些原则,用最优雅的方式去完成实现。
前端技术发展日新月异,前端可以解决的问题越来越复杂,覆盖面越来越广,未来前端的地位将会越来越重。
评论