EmberJS Ember Data 概述

nightire · 2015年12月06日 · 最后由 jesktop 回复于 2015年12月23日 · 11259 次阅读
本帖已被管理员设置为精华贴

呃……前面那个坑还没填完,因为我又改变了一些想法还在试验中。

这是一个小坑,是两个月前给新团队的小伙伴们入门 Ember Data 时写的大纲,这两个月下来我发现还是挺有用的,先放出来以备后面挖大坑用……注意,这份大纲编写的时候 Ember Data 才刚刚 2.0 beta,到今天已经有了一些变化,而且官方的 API 文档做了很多补全或修正,所以实际应用还是要多看文档,以它为准。

基本概念

  • Data Store:前端的数据仓库,类似于后端的数据库,但数据库是物理存储的实体且没有对象关系(需要语言提供 ORM 来实现对象关系映射),而前端的数据仓库则是随着应用程序的启动而实例化在内存中的动态仓库。它是有时效性的(浏览器关闭就没有了,因此它要实现与后端 API 的通信来存储数据),而且它很快(运行在内存中),另外它是可以管理对象关系的。简而言之,数据仓库就是把后端 API 返回的数据(如:JSON)转换为 JavaScript 对象(Ember Object),并映射和管理对象之间的关系(类似后端的 ORM),同时它也负责保存自前端创建的数据对象,并完成向后台发起请求保存数据的工作
  • Ember Data records:记录,即数据实体;可类比为后端数据库里的一条记录,只不过前端没有数据库实体,而是运行在内存中的 Store

主要组成部分

DS.Store

  • Store 保存了自服务器取回的所有数据(保存在客户端)
  • Store 同时负责在客户端创建定义为 DS.Model 的数据实体,为的是让这样的数据实体可以和 Handlebars 模版进行数据绑定

DS.Model

  • 所有的 Ember Data records 都是 Model 的后代,当使用 Ember Data 配合 Ember 的时候,应该使用它来定义所有的 models
  • 除此之外还有一个 DS.InternalModel,专门用来处理 Ember Data 内部的数据模型,但不应用来开发 Ember 应用里的数据模型。这是因为 DS.Model 产生的数据实体是有数据绑定等能力的,适合于 UI 编程(与模版视图进行数据绑定);而 DS.InternalModel 处理的数据实体都是纯 JavaScript 对象,只适合 Ember Data 内部(不需要和模版视图进行数据绑定)。也因此,DS.InternalModel 要比 DS.Model 快些

DS.Snapshot

  • Snapshot:快照,即数据实体在 Ember 应用程序内的载体,除了数据本身之外快照保存了数据实体当前的状态(如是否有更改等)以及数据实体的关系等。

DS.RootState

  • 每一条数据实体都有一个 currentState 属性,它显式的追踪着数据实体在任意时间点下的状态。比如说一条刚创建并且没有保存(即提交至后端)的数据实体,其状态是 root.loaded.created.uncommitted 等等
  • 一些事件会通过数据实体自己或者数据仓库发送给 currentState 属性,数据实体对这些事件如何反应取决于当前所处的状态。在不同的状态下,一些事件是无效的并且会抛出相应的异常(可用来追踪非法操作,比如说标注为 删除状态 的数据实体不能不访问或修改等等)
  • 状态是分级管理的,并且所有的状态都是 RootState 的子状态。比方说,一条数据实体可以从 root.deleted.uncommitted 状态转换至 root.deleted.inFlight 状态等等。如果一个子状态没有响应的事件回调函数,状态管理会尝试去调用它所有的父级状态的事件回调函数来处理它,直到根级状态。
  • 状态层级使用路径式的字符串来表示,你可以访问数据实体的 stateName 属性来获取当前状态的完整路径。例如:record.get('currentState.stateName')
  • Ember Data 提供的默认状态(分级)如下表所示:
    • root
      • deleted
        • saved
        • uncommitted
        • inFlight
      • empty
      • loaded
        • created
          • uncommitted
          • inFlight
        • saved
        • updated
          • uncommitted
          • inFlight
      • loading
  • DS.Model 的状态其本身是“无状态的”,这个意思是说:分层式的状态数据结构在全局中只有一份(类似单例对象),每一个状态所指向的都是这个不可改变且全局共享的数据结构(immutable and global shared data structure)
  • 这种设计是出于提高性能的考量,可是状态管理如何知道当前是哪一个数据实体处在哪一个状态中呢?Ember Data 会在状态事件回调函数中把当前数据实体作为第一个参数传递给状态管理机制

事件和旗标(Events and Flags)

一个状态可能实现零或多个事件和旗标

事件

  • 事件是具名的回调函数,当数据实体收到事件时会被调用
  • 数据实体先寻找自身对应的事件回调函数,如果没找到就向上(沿着状态层级)寻找父级的事件回调函数,依此类推直到根级状态(root state)
  • 若是根级状态也没有对应的事件回调函数,就会抛出一个异常
  • 这样的机制有助于调试新的功能特性
  • 没有意外的情况下,事件回调函数应该转换当前的状态到一个新的状态去,这可以通过调用数据实体的 transitionTo 方法来完成
  • 状态转换发生时,状态管理机制会尝试循着当前状态的路径向上查找,类似于寻找事件回调函数的处理方式

为了便于理解,举一个例子来说明,想象当前有这样一个状态结构:

  • created
    • uncommitted <- currentState
    • inFlight
  • updated
    • inFlight

接着,如果调用 record.transitionTo('inFlight'),那么状态将会转换至 created.inFlight;如果调用 record.transitionTo('updated.inFlight'),那么状态会转换至 updated.inFlight

切记!状态转换必须发生在事件回调函数内,永远不要在事件回调函数外调用 transitionTo 方法;如果你确实需要,就创建一个新的自定义事件并发送给状态管理机制(于是这个事件的回调函数就能用来调用 transitionTo 方法了)

旗标

旗标就是一系列的布尔类型的属性,为了让你方便的查询数据实体当前的状态(而不是解析路径字符串)。比方说,虽然你可以这样做:

let statePath = record.get('stateManager.currentPath')
if (statePath === 'created.inFlight') {
  doSomething()
}

但是,这样做会更方便:

if (record.get('isNew') && record.get('isSaving')) {
  doSomething()
}

以上例子也暗示了一件事:旗标与状态并非一一对应的(虽然有时候是),一个状态可能是由多个旗标共同定义的。

如果一个状态没有设置对应的旗标,那么旗标的值会继承父级状态对应的旗标,或者是之前的状态设置的值。

以下是默认的旗标,它们的说明可以在 DS.Model 的文档里找到。如果你需要定义新的旗标,需要在 DS.Model 里定义新的属性。

  • isEmpty
  • isLoading
  • isLoaded
  • isDirty
  • isSaving
  • isDeleted
  • isNew
  • isValid

DS.Transform

根据官方文档的描述,Ember Data 默认允许我们定义四种数据类型的数据属性,它们分别是:string number booleandate。有时候我们需要自定义类型的数据属性,我们可以利用 DS.Transform 接口来进行扩展。它的简单用法如下:

// app/transforms/temperature.js
import DS from 'ember-data'

// 转换 JSON 里的摄氏度数,变成应用里的华氏度数
export default DS.Transform.extend({
  deserialize: function(serialized) {
    return (serialized *  1.8) + 32
  },
  serialize: function(deserialized) {
    return (deserialized - 32) / 1.8
  }
})
// app/models/requirement.js

import DS from 'ember-data'

export default DS.Model.extend({
  name: DS.attr('string'),
  temperature: DS.attr('temperature')   // 实际应用
})

DS.Adapter

  • Adapter(适配器)用于接收来自数据仓库发出的请求(requests)并将其转换成对应数据持久层(通常是 API Service)的 HTTP 请求或其他合适的请求(比如说用 LocalStorage 做持久层的时候,就不需要 HTTP 请求了)
  • 通常我们不会直接调用 Adapter,而是通过 Store 提供的高层 API
  • Adapter 是一个抽象的基础类,当你定义新的 Adapter 的时候,目标就是让其适应你的后端服务。一套最小的实现应该包含如下方法:
    • findRecord()
    • createRecord()
    • updateRecord()
    • deleteRecord()
    • findAll()
    • query()
  • 有时为了提高网络通信的性能,你也可以重载 findMany() 方法来优化你的 Adapter
  • Ember Data 默认提供了通用的 DS.RESTAdapter,适用于标准的 REST API Service,你可以重载其中的方法来适应你的(非标准的)REST API Service
  • Ember Data v2.0 后,默认(并且推荐)的 Adapter 将会是 DS.JSONAPIAdapterJSON API 规范 是基于 REST 架构的新一代的标准,它定义和规范了开发基于 JSON 的 REST API 的一系列标准和实践准则
  • 此外,还有一个 DS.DebugAdapter,这是 Ember Data 内部用于调试的适配器,通常不需要考虑它

DS.BuildURLMixinDS.EmbeddedRecordsMixin

这两个 Mixins 用于为 Adapter 实现两种功能:

  1. 构造合适的 URL(为 API Service)
  2. 处理嵌套式数据资源

具体用法可以参考 DS.RESTAdapterDS.JSONAPIAdapter

DS.Serializer

  • Serializer(序列器)是和 Adapter(适配器)类似的抽象基础类,不过它用来转换请求或响应的数据格式,而不是匹配请求的方法
  • 一套最下的实现应该包含如下方法:
    • normalizeResponse()
    • serialize()
    • normalize()(可选)
  • Ember Data 内置了通用的 DS.JSONSerializer,以及对应 REST 和 JSON API 适配器的 DS.RESTSerializerDS.JSONAPISerializer。需要定义适合自己 API Service 的序列器就可以参考以上的例子或者直接继承 DS.JSONSerializer

举一个常见的例子,假设你有这样的 data model:

App.User = DS.Model.extend({
  name: DS.attr(),
  friends: DS.hasMany('user'),
  house: DS.belongsTo('location'),
})

此时如果直接使用 DS.JSONSerializer,那么请求或响应获得的 JSON 数据应该是这样的:

{
  id: 1,
  name: 'Sebastian',
  friends: [3, 4],
  links: {
    house: '/houses/lefkada'
  }
}

这是很常规的 JSON 数据格式,如果你的服务器与此不符,那么你可以扩展 DS.JSONSerializer 来重新定义。

DS.Errors

  • DS.Errors 是 Ember Data 提供的异常和错误处理的基础类
  • 它主要用于保存模型的有效性验证错误(validation errors),使用模型的属性作为组织结构
  • 每一个 DS.Model 都有一个 errors 属性,其值就是一个 DS.Errors 的实例对象,可以用来显示服务端返回的验证错误信息
  • DS.ActiveModelAdapter(以前版本里自带的适配器,用于适配 Rails 框架,现已被抽取出去变成了独立的 Addon)实现了这一套机制,可以自动处理 Rails 返回的错误信息。如果你要定义适合自己 API Service 的错误信息,可以参考它的实现
  • 目前,Ember Data 的新版本正在扩展 DS.Errors 以期提供更多的默认类,文档尚未补全

👍

准备好小板凳

在楼主的帖子里总能学到很多姿势 :plus1:

lz 已经横屏了

如果接口不是 Restful 的 EmberData 好像就没啥用了

#5 楼 @tangmonk 也不尽然,Adapter 和 Serializer 是可以自己扩展的,刚接触的时候觉得很难,实际做一下也就那么回事。只不过非 Restful 的接口在 endpoint 上都没有什么规律可言,如果设计不当很难去做抽象罢了。所以你说 Ember Data 没用也没错,但更准确的说是你没这个机会享用其好处。对有能力者而言,理解了它所做的事情也可以自己实现一套,有没有它都不要紧。

#5 楼 @tangmonk 不是的,你发送 ajax 请求拿到 json, 可用手动调用 store 对象上pushPayload方法帮你 push 到 Ember Data,这样还能正确处理 relationship

Events and Flags 的使用场景是什么能分享下么,我发现最常用也就是查查 Model 的状态,比如 isNew,isDirty 什么的; 另外,由于 InternalModel 的存在,Ember Model 可以 rollback,这个在表单需要重置所有修改的时候,简直太好用了。

#8 楼 @zhenhuaa 实际应用的开发中,旗标用得多,状态主要用于 Addons 的开发,找一些和 Data Store 相关的 Addon 看看源码能发现很多运用场景。

最近看了一下JSON API 规范,和原来的生成 json 的方式很不一样,而且规范强调不少东西是必须的,这样显得更加严谨。可是目前不知道还有谁在使用这套规范的,还没有体会到JSON API 规范特别大的意思在哪。

#10 楼 @jesktop 我在上一个团队了帮助后端实施了 API 的重构,搞了一次 JSON API,你所顾虑的问题我们在这个过程中都有遇到,以下是我个人的一些体会:

  1. 和原来的方式很不一样——那主要是因为原来的方式根本没有规范可言,即便有可适用范围也是很局限的(多是一个项目或一家公司范围内的)
  2. 强调必须的东西实际上并不多,虽然规范里满眼的 MUST,可实际做下来看看倒是觉得真是如此,没有哪个是多余的。
  3. 实际使用的项目不算多,毕竟 1.0 刚放出来,不过也是有的。JSON API 的 Github 里专门有一个 issue 搜集了一些,连泰国的项目都有……
  4. 特别大的意思其实还是不少的,这里我就说一个:客户端的 Adapter。像 Ember Data 这样的 Data Store 是希望做到“非特定性”的,也就是并不是非得某种框架的 API Service 才能即插即用。然而这在过去并不太容易,所以以前才会有 ActiveModelAdapter,RESTAdapter 等等,这也没办法毕竟每个框架都有自己的 conventions,每个团队都有自己的一套。以前也不是没有统一的举措,JSON API 也未必就是最终的一个,但这样的规范总还是要有的。因为客户端在前进,以前那种直接操作 Raw Data 的方式已经落后了……这里面的细节我就不多说了,理想的结果是像 Ember Data 这样的只需要一个 JSONAPIAdapter 就够了,不管后端用什么框架工具,输入输出的结果的都是一致的;同样别的客户端也会有对应 JSON API 的 Data Store,大家都是一劳永逸不好么?

@nightire 你说的 JSON API 实用场景我挺感兴趣。可否详细讲一下?我比较关注的几点:

  1. 和 Ruby 框架的集成是否简单?(有一个 Orbit 作者他哥写的 Ruby gem,我感觉有点重了)
  2. 对细粒度一点的 API 好用么?比如只更改局部数据而不是整个 resource 的
  3. 涉及到多个 resource 的保存如何处理的?

#12 楼 @darkbaby123 我也在用 JSON API,我能体会到的好处大概有:

  1. 格式丰富而且统一,基本上覆盖到了绝大多数需要的数据类型。一般数据在 data 里,分页有 links,associations 有 relationship,错误有 error,统计类的可以放到 meta 里。各归其所,简直不能太舒心。
  2. 便于团队协作。基于上述原因,Android,iOS,web 都可以遵循同样的规范,最大限度地减少了后端的 parsing 工作,不必再自定义一套接口协议。大赞!
  3. Rails 集成度不完美但够用,并且也是官方推荐。因为这个规范比较新,目前还是不能很好处理 associations(也有可能是我们团队里的 bug,这个锅也不一定是 jsonapi serializer 的),但我觉得这个可以适当的做折衷。

大概就是这些了。

#12 楼 @darkbaby123 就像 @ugoa 所说,刚出来的东西配套不够完善,我们之前是做了一个 Spring 的实现,的确也遇到不少问题。

  1. 你说的那个 gem 我没用过,不知道重到什么程度。Rails 的话其实 ActiveModel::Serializers 就可以了,只不过这货不更新了?还是准备并到 Rails 5 做 default 了?其他的 Ruby 实现很多啊,看这里:http://jsonapi.org/implementations/#server-libraries-ruby,要不你来调研一下吧,哈哈

  2. 细粒度不是问题,获取数据规范定义了 Sparse Fieldsets,更改(局部)数据规范明确定义了 PATCH 的用法

  3. 对于批量动作,JSON API 是使用扩展来支持的,扩展的好处就是隔离特例和常规用途,并且允许第三方定义和实现扩展。批量动作是一个官方扩展:http://jsonapi.org/extensions/bulk/,问题是各种 implementations 是否支持了官方的扩展这就不一定了,得看它们的实现程度和未来计划。

@nightire 嗯,我说的是 JSONAPI::Resources ,文档和功能倒是很全,不过又抽了 ResourceSerializer 两层,加上 Rails 默认的三层感觉有点复杂。毕竟 API 规范也只是整个系统中的一小部分,我比较倾向于能让事情变得更简单而不是更复杂的方案。ActiveModel::Serializers 我只用过 0.8 版,经验自然是没你丰富。还等着你什么时候分享 JSON API 的干货呢。

看了 Sparse Fieldsets 和 Patch,一个处理 response 一个处理 request,挺清晰的。谢谢!

#15 楼 @darkbaby123 貌似只能在ActiveModel::SerializersJSONAPI::Resources之间选择,JSONAPI::Resources感觉确实太重了,提供了自己 route 的 resource 方法挺不错的,但是它把 controller 那块拿出来了,如果是非常标准的 RESTful,就真心方便,但是要自己定义 action 瞬间变得很复杂。 ActiveModel::Serializers就好多关于 JSON API 的东西还等着 PR 的,而且文档很不清晰,虽是 0.10.0 的 RC 版本,但是相对与文档去使用,还是相当多的东西用不了的。 感觉只是一个标准而已,丢出那么多层东西不太合适。我也期待楼主分享一下 JSON API 这东西的干货。 😃

需要 登录 后方可回复, 如果你还没有账号请 注册新账号