呃……前面那个坑还没填完,因为我又改变了一些想法还在试验中。
这是一个小坑,是两个月前给新团队的小伙伴们入门 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
- created
- loading
- deleted
- root
-
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
boolean
和 date
。有时候我们需要自定义类型的数据属性,我们可以利用 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.JSONAPIAdapter
,JSON API 规范 是基于 REST 架构的新一代的标准,它定义和规范了开发基于 JSON 的 REST API 的一系列标准和实践准则 - 此外,还有一个
DS.DebugAdapter
,这是 Ember Data 内部用于调试的适配器,通常不需要考虑它
DS.BuildURLMixin
和 DS.EmbeddedRecordsMixin
这两个 Mixins 用于为 Adapter 实现两种功能:
- 构造合适的 URL(为 API Service)
- 处理嵌套式数据资源
具体用法可以参考 DS.RESTAdapter
或 DS.JSONAPIAdapter
。
DS.Serializer
- Serializer(序列器)是和 Adapter(适配器)类似的抽象基础类,不过它用来转换请求或响应的数据格式,而不是匹配请求的方法
- 一套最下的实现应该包含如下方法:
normalizeResponse()
serialize()
-
normalize()
(可选)
- Ember Data 内置了通用的
DS.JSONSerializer
,以及对应 REST 和 JSON API 适配器的DS.RESTSerializer
和DS.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
以期提供更多的默认类,文档尚未补全