前端框架和技术实践汇总
这段时间在拿一个小项目练手,力图寻找出一个适合自己的前端框架合集。我们知道 Vue 框架为我们实现了数据绑定和单页应用,让我们能够基于数据状态而不是 DOM 状态开发前端项目。但我们仍然知道,Vue 只是实现了视图层的逻辑而已,围绕 Vue 框架我们依然需要为前端项目的其他层次选择合适的技术方案。
就我个人观念而言,前端项目的以下技术方案至关重要:
- 视图层的数据绑定,这个可以由 Vue 框架帮我们做到
 - 表单验证
 - 与后端 API 的交互
 
这篇文章将介绍后两者的方案。
与后端 API 的交互
在与后端 API 的交互过程中,我选择了 js-data 作为我的实现载体。js-data 是一个 JavaScript 框架,起初为 Angular 框架开发,现在能够适配任意框架。当然,在应用于 Vue 的时候还有小小的不适应性,待会我会提到。
除了可以调用后端提供的 API 之外,js-data 提供了模型层几乎需要的东西,包括:
- 模式定义
 - 关系映射
 - 数据缓存
 - 多数据源的支持
 
模式定义
模式定义为模型定义模式,这样约束了模型的字段,同时还可以为其做数据验证。例如我们定义了以下的模式:
1  | const userSchema = new Schema({  | 
我们只能为 user 对象提供 id、name、age 参数,并且如果它们的类型不一致也会报错:
1  | new User({  | 
关系映射
关系映射是四个东西里我最不在意的,因为它带来的便利仅仅是编码更简洁了(也许是我没有理解透彻,望看官指正)。首先我们可以如下定义两个模型之间的关系:
1  | store.defineMapper('user', {  | 
便利性的体现有很多,这里我仅举两个较容易理解的场景:
场景一:同时创建
1  | await store.create('user', {  | 
场景二:同时返回
1  | await store.find('user', 1, { with: 'movies' })  | 
将返回:
1  | {  | 
我说过四个东西里最不在意的就是“关系映射”,但这里却用了最大的篇幅介绍这货,这说明关系映射确实是这里面最复杂的(而不是我对它有什么偏赖)。不在意是因为即使没有定义关系,我们仍然可以用别的方式实现(显而易见),只不过是样板代码稍微多了点。
1  | // 针对场景一  | 
说实话,在开发前端项目时,以上写法也许更常见。
数据缓存
当我们通过调用后端 API(更一般地,从数据源)取得数据时,再度请求同样的数据会发生什么?例如在列表页里我们已经取得 posts 列表数据,如今点击其中一项进入详情页,此时详情所需的 post 数据已经在 posts 列表内了,我们有必要再请求一遍吗?当然不必要了,可现实是我们往往又重新请求了一遍,完全没有用到缓存机制。
vuex 尝试解决这个问题。但说实话,这种全局状态绑定的模式我是真心喜欢不起来。
js-data 通过缓存机制无感地为我们解决了这个问题。首先,我们在列表页已经请求过一遍数据:
1  | await findAll('post')  | 
然后点击其中一项进入详情页,请求 post 详情数据依然用同样的调用方式:
1  | await find('post', 1)  | 
代码没有任何变化,内在逻辑的改变 js-data 悄悄地为我们做了。如果我们是从列表页进入详情页的,会从缓存中拿到数据而不会再向后端发送请求;如果我们是直接进入详情页的,会向后端发送请求。
多数据源的支持
数据存储在哪里?数据从哪里获取?对于前端项目来说,大多数不是一个问题:数据当然是通过调用后端 API 获取的。js-data 同时提供多数据源的支持,利用适配器模式,几乎不用修改任何代码就可以将数据源从后端服务器切换为 IndexedDB. 你可以不用,但有时候也许用得上。
表单验证
虽然 js-data 为我们提供了数据验证的功能,但数据验证不同于表单验证。表单验证面向的是终端用户,为用户提供即时的反馈。
基于 Vue 的控件多数提供了表单验证的功能,例如 Element UI、iView 等,如果你用的是这类 UI 组件,直接使用它们的表单验证机制即可。
有时候表单控件没有提供验证的能力,这时我们可以使用 async-validator 包配置我们的验证逻辑。
js-data 的槽点和坑
我们有了 js-data,表示我们有了模型层的封装。我们有了表单验证,为我们解决了一个开发上的大难题。剩下的就交给 Vue 框架和 Vue 生态(例如 vue-router)。有了这三大利器,我想不到还缺少什么(我指的是几乎所有项目都需要的那种东西)。
js-data 占了很大的一个比重,但它不是完美的。就我的实践发现,它主要有两个坑:
面向的不是普通的 JavaScript 对象
store.findAll 返回不是对象数组,而是 Record 实例数组。
1  | const users = await store.findAll('user')  | 
这有什么关系呢?用在 Vue 框架就大大的有关系了,因为 Vue 框架有个明确的建议:只能用于普通的 JavaScript 对象。如果绑定的不是一个纯粹的 JavaScript 对象,而是一个类的实例,有可能遇到潜在的问题。不幸的是,js-data 正好中枪了。
解决的办法是:将 Record 转化为普通的对象。Record 提供了 toJSON 方法,它将会转化为普通的对象:
1  | let users = await store.findAll('user')  | 
Record 会提供额外的便利的方法,有时候我们会用到它。况且每次请求时调用 toJSON 方法转一道不觉得繁琐吗?所以我在实践中会采用折衷的办法,有时候使用 Record,有时候使用普通的对象。
那么何时使用 Record,何时又要转化为普通的对象呢?其窍门在于,我们何时用何种方式触发 Vue 的响应式。因此,我得出的结论是:在仅展示的页面使用 Record,在数据会被修改的地方使用普通的 JavaScript 对象。
列表页是一个仅展示的页面,它在初始化时触发响应式:
1  | async mounted () {  | 
而在编辑页模型的内部状态会被修改,不适用于 Record:
1  | <template>  | 
它最佳的适用模式是数据源拥有一套标准的 CURD 方法
针对数据源的操作无非是 Create、Update、Retrieve、Delete 四种模式。无论是关系型数据库、文档型数据库、Redis、ElasticSearch、localStorage、WebSQL、IndexedDB 等,它们都有一套标准的 CURD 方法,为它们编写适配器是一种统一的工作。这也是 js-data 也能应用于后端的原因。
而前端最常面对的是后端提供的 API,它没有统一的 CURD 实现标准。试想一下,后端 API 的风格千奇百怪,尤其是中国这个特殊的大环境下。所以,不同的项目有不同的实现标准,甚至同一个项目的不同模块都有不同的实现标准,这是我们在应用 js-data 最大的难题。
另一个方面,由于后端提供的 API 是面向业务逻辑的,它提供的接口未闭会完全参考 CURD 的模式。例如,有一个审核的接口,它可能定义为 /posts/{id}/verify,可以传递一个 value 参数为 true 或 false,以使得这篇博文审核被通过或被拒绝:
1  | curl -XPOST /posts/1/verify -d '{ value: true }' # 审核通过  | 
它的主要作用是修改 post 资源的 verified 字段,也许会有其他附加的动作如添加审核人和审核时间等。无论如何,它已经被设计为 Restful 资源下的一个动作了,关于这一点,js-data 的扩展能力及其之低。
我在前面说过,我做了一个小项目实验 js-data 的特性,由于都是标准的 CURD 方法,并且数据源是本地的 IndexedDB,所以也就没有遇到这个潜在的问题了。但我隐隐觉得这可能是应用 js-data 最力不从心的地方,后续实践进一步验证。
总结
总结一下前端项目如何实践:
- 适用 Vue 作为视图层数据状态的绑定和单页逻辑。
 - 添加表单验证,它们或是 Vue 组件自带的功能,或用 
async-validator库自行实现一套。 - 适用 
js-data作为与数据源的交互,以及定制模型层。 
我们还提到 js-data 并不是完美的,但是它提供的几个特性如模式定义、数据缓存是我们在开发中必不可少的。并没有任何东西是完美的,Vue 也有很多不完美的地方,但开发的任务却是不容等待。在我们的认知局限下选择最适合我们的方式,才是当务之机。