前端数据管理与框架选择

这篇文章源于一场知乎live,通过这场live整理了前端数据管理的发展历程,总结了一些常用的框架。

从无到有

前端诞生之初,只是作为一个文档出现,可以进行一些基础的排版,而交互仅限于表单提交。终端的计算能力差导致逻辑主要由后端处理,并且很长时间都耗费在兼容之中,网速也成为限制,更倾向于在终端直接获取结果,而不是拿到数据再在前端渲染,前端发展很慢,且对后端依赖很强(后端数据格式不对,只能等待)。页面之间也互相没有什么联系,路由控制在后端。

Ajax 普及之后,前端可以灵活的拉取数据,不需要一次全部加载,从此开始有了管理数据的需求,但是刚开始也是拿到数据后直接用(修改 DOM)。但是实际情况复杂的多,发起请求的地方有很多,请求回来对 DOM 的操作又有很多。

随着 ajax 请求越来越多,各路数据各自为战,变得不能接受,开始考虑把所有数据放在一起管理,再去用数据更新 DOM/组件,同时可以考虑用一个全局对象来管理数据。目前为止还没有框架,只有思想。

前端的MVC方案

16年入行的我,已经很少听说/使用这种框架了,但作为历史发展的一个阶段还是要了解一下。

Backbone.js 是一个早期的著名框架,在前端完整的实现了 MVC,提供了一个 model 类,以对象(也就是map)的形式存储数据,同时还有一个 Collection(集合)方法,存储模型的有序组合。相当于把数据库复制了一份到本地,本地提供 CRUD 各种方法去对接,Backbone 突破点在于把路由放在了前端,这样就可以整个数据层(或者不能叫数据层,而只是接口层)也放在前端(从而实现了业务数据的集中管理)。MVC 搬到前端解决了一些问题,前端可以像操作数据库一样在页面获取和存储数据,但是很快发现这种结构除了从后端转移了更多工作量以外,实际上没有解决很多核心问题。

前端数据的核心问题在于,前端的很多数据只是用来记录状态的,对于后端来说,这种状态变量大部分都是临时的,从库里取出来后只是临时辅助使用,而对于前端来说就很重要,因为它决定了视图层,这些变量往往和业务关联,状态之间也会互相关联。在 jquery 时代,用命令式处理这些变量,比如在 click 事件里让弹窗显示/隐藏,当这些逻辑变多起来后,我们开始疲于应对各种响应事件,业务逻辑和结构逻辑混在了一起。于是又有新的思想开始出现。

UI=render(data)

Backbone MVC的结构中,直接照搬后端数据的问题在于,要把业务数据在C层组装,这样是不利于复用的(这里其实我有点不明白),而且方案中的DOM操作还是类似 jQuery 的处理,命令式的操作 DOM,修改状态(比如修改一个模态框的显示/隐藏等等)。

但其实我们发现,这些不适合放在数据库中读写,也不具备表结构的状态,同样是一种数据,并且没有被很好的管理,而且既然数据决定了 DOM,那么我们是不是没有必要非用命令式的方式去操作 DOM 呢?所以新的数据驱动式方案应运而生,状态作为数据被集中管理(state),并且可以驱动视图变化,这种新思想也可以叫做——数据驱动 UI(或者也可以叫一种数据的可视化,实际上前端就是在做 json 的可视化):

  • jquery:命令式操作DOM
  • Vue/React:不关心DOM,只关心数据,只要数据改变,DOM就变化

比如一个弹窗的显示,jQuery 的操作是增加一个 DOM,删除一个 DOM,而数据驱动式框架中,只是一个 state 的 true/false 变化,至于 DOM 和变量之间的关联由框架完成(Vue 中自动完成,React 中使用 setState),总之关注点由 DOM 移到了数据。使得复用也可以通过组件化解决,把业务所需的视图做成标准件,对外依赖各种 state/props ,使得前端开发更加工程化。可以看到方案之间是相辅相成的,数据层面的集中也导致了视图层面的集中,也就是组件化。

组件间通信

前面说过数据分为业务数据和状态数据,状态数据大部分是和UI相关的,比如说单选框是否被勾选、按钮是否可以点击,但是这些都是组件内状态数据,他们不需要理解业务逻辑,只是业务逻辑的映射,React 中有一种组件叫木偶组件(或者叫傻瓜组件,对应的另一种叫容器组件),组件库提供的都是木偶组件,木偶组件是有简单状态或者无状态的,数据几乎全部依赖于输入。

还有另一种状态数据是跨组件的状态数据,并不是数据库中业务数据的映射,但是在前端交互中十分重要,比方说三级联动的地区选择器,一旦需要跨组件的交互,我们就需要一个通信机制解决父子/兄弟组件之间的通信,父对子就是 props,子对父可以做双向绑定(但是不推荐,vue2.0 已去掉),React 的处理方式(Vue 也可以)是父组件传递给子组件一个处理函数,但是这种处理方式并不优雅(层次过多则需要通过 props 层层传递)。我们需要比之前组件内状态(即 state)更大范围的数据集中管理。

至此,我们的数据管理又上了一个台阶,来到了一个新的世界:SPA应用的全局的数据管理(包括跨组件、跨页面等情况)。这里我们会遇到 Flux、EventBus、Redux、Mobx 等等一系列框架,live 后半部分都是在讲这些,内容太多我这里暂时不展开讲了,下面说一些我的思考。

下一个问题

React 是一个 MVC 框架吗?那肯定不是的,我们先定义一下 MVC 再做讨论:

  • Model (模型)负责管理数据 ,大部分业务逻辑也应该放在 Model 中;
  • View (视图)负责渲染用户界面,应该避免在 View 中涉及业务逻辑;
  • Controller (控制器)负责接受用户输入,根据用户输入调用对应的 Model 部分逻辑,把产生的数据结果交给 View 部分,让 View 渲染出必要的输出。

在 MVC ( Model-View-Controller )的世界里, React 相当于V(也就是 View )的部分, 只涉及页面的渲染一旦涉及应用的数据管理部分,还是交给 Model 和 Controller 。但如果你只有一个页面,并且页面全部写在一个文件里,全部靠 state 维护的话,你勉强可以把 state 算成 Model(这个 Model 也太弱了),render 部分算作 View,但是实在找不出 Controller 了,而且我觉得这样强行类比也是有问题的,大部分情况作为一个应用我们不可能只有一个页面,只有一个页面也没必要非要上 React 了,也难以称得上是一个应用了,更没有MVC了。

React — A JavaScript library for building user interfaces(React 官网定义)。

所以我认为,React 应该只负责 View 层,并且是用来替换 jQuery 的,而 Flux 等框架才是以替换 Backbone.js 等MVC框架为目的,所以 React + Flux/Redux 才能勉强称得上MVC。对于 MVC 框架,为了让数据流可控,Controller 应该是中心,当View 要传递消息给 Model 时,应该调用 Controller 的方法,同样,当 Model 要更新 View 时,也应该通过 Controller 引发新的渲染,然而实际操作时经常不按流程来,所以 React + Flux/Redux 利用严格的单向数据流实现了 MVC(这么说也有很多问题,比如如果我们把 Dispatcher/Reducer 看做 C,那相比传统 MVC 框架没有负责路由,相比前端 MVC 又没有直接控制视图,而是通过控制数据控制视图,所以一般也不这么说,直接说单向数据流就好了)。

那么 Vue 是一个 MVVM 框架吗?这个就有很多疑惑了,因为很多人都这么认为,我们同样先定义一下MVVM:

  • Model(模型)是数据和逻辑;
  • View(视图)是用户在屏幕上看到的结构、布局和外观,也称UI;
  • ViewModel(视图模型)是一个绑定器,能和 View 层和 Model 层进行通信。

MVVM 的核心实现是由 ViewModel 层数据绑定,它的核心思想是分离,也就是通过 ViewModel 让 View 层和 Model 层解耦,这样有什么好处呢? View 层不直接和 Model 层通信,他们只能通过 ViewModel 层通信。我们在 Vue 里套用一下 MVVM 的概念:

  • View:单文件里 <template> 标签的内容,展现给用户的内容,与 ViewModel 双向绑定,可以在其中插入 ViewModel 提供的数据;
  • ViewModel:Vue 实例整个都是 ViewModel,与 View 双向绑定,用户在 View 修改数据或发出 ajax 等指令时, ViewModel 会及时相应,接着向下修改 Model——至此可以看出 Model 和 View 是没有直接关系的;
  • Model:这一层或者有歧义。为了更好理解 Model 需要引入 Vuex,在有 Vuex 的情况下,Vuex 提供的数据就是 Model,这符合后端架构中 Model 包含业务逻辑的情况。但是在无 Vuex 的情况下,Model 应该就是 Vue 实例的 data 属性,也就是 JavaScript 数据对象本身。

认真看过 Vue 文档大概都能注意到,Vue 实例的变量名是 vm,文档中还很严谨地补充了一句 “虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发”。所以官方定调是不是,有些对于 Vue 违背MVVM的说法是:

严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 在组件提供了 $refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

至于对不对,我也不置可否,毕竟:

If you put ten software architects into a room and have them discuss what the Model-View-Controller pattern is, you will end up with twelve different opinions. — Josh Smith

所以这是个开放性的问题,没有标准答案,言之有理即可。

总结

从最简单的页面的数据处理,到复杂系统如何管理数据、状态与样式,随着前端的发展,我们开始使用越来越复杂的框架,状态管理和组件化也一同迅速发展,随之而来的还有各种数据存储和管理的方案与框架。如何选择使用合适的方式管理前端数据,以及对框架进行选择,成为了现代前端都需要去考虑的问题。

参考链接

前端数据管理与前端框架选择

为什么尤雨溪尤大说VUE没有完全遵循MVVM?

浅析 web 前端 MVVM