Emberjs——Ember Data介绍
英文原文地址:https://github.com/emberjs/data
Ember Data 是一个用于加载、更新模型的库,模型来自持久层(如从JSON API获取),更新完成后保存更改。它提供了许多专门为浏览器端的JavaScript环境设计的工具方法。
当前是测试版本,但能完成基本任务,对于特殊情况还不能处理。
创建一个Store(存储器)实例
每个应用都有一个或多个存储器。存储器将成为已加载模型的贮存器,并且检索还未被加载的模型。
创建store(存储器)实例的代码如下:
App.store = DS.Store.create({ revision: 6 });
注:revision属性是在1.0版本之前,ember-data用于提醒你公共API的重大更改。对于新的应用,只需要将revision设置为这个数字即可。查看BREAKING_CHANGES.md(https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md)以获取更多信息。
你可以通过指定一个adapter(适配器)告诉你的store如何与后端交互。Ember Data带有一个RESTful JSON API适配器。你可以通过adapter属性指定该适配器:
App.store = DS.Store.create({ revision: 6, adapter: DS.RESTAdapter.create({ bulkCommit: false }) });
RESTAdapter支持下面的选项:
·bulkCommit (默认值:false):批量提交。如果你的REST API不知持批量操作,可以通过设置该值为false来关闭。
·namespace (默认值:undefined):命名空间。A leading URL component under which all REST URLs reside, without leading slash, e.g. api (for /api/authors/1-type URLs).[此句没看明白]。
·url (默认值:""):一个为位于不同url或子域的API自定义的url,使用包括http在内的有效域名。例如“http://api.emberjs.com”。注:仅在服务器与浏览器支持CORS(维基百科--http://zh.wikipedia.org/wiki/CORS)时才能工作。
RESTful还在开发中。
定义Models(模型)
通过创建DS.Model的子类来表示每一种数据类型:
App.Person = DS.Model.extend();
通过DS.attr指定模型拥有的任一属性的attributes(暂翻译为属性特征)。属性特征表示的值将存在于底层的JSON对象中,同时也可通过Ember对象显示出来。
你可以像使用其他属性一样使用属性特征,包括作为计算属性的一部分。这些属性特征确保属性值可以从底层JSON对象中被检索并作为后续需要持久保存。
示例代码:
App.Person = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), birthday: DS.attr('date'), fullName: function() { return this.get('firstName') + ' ' + this.get('lastName'); }.property('firstName', 'lastName') });
有效的属性类型有string,number,boolean,date。你还可以注册自定义的属性类型。例如,这儿有一个属性类型boolString,用于转换布尔值为字符串“Y”或“N”:
DS.attr.transforms.boolString = { from: function(serialized) { if (serialized === 'Y') { return true; } return false; }, to: function(deserialized) { if (deserialized) { return "Y"; } return "N"; } }
默认情况下,store(首字母小写表示类的实例)使用model的id属性作为主键,这可以通过设置父类的primaryKey属性来指定一个不同的主键:
DS.Model.extend({ primaryKey: 'guid' });
关联模型
模型可以与其他模型关联起来。Ember Data包括了几个内置的方法,以帮助你确定你的模型如何相互关联的。
Belongs To 方法
belongsTo设置了一对一的关系,在下面的例子中,不仅能找到author的profile,还可以找到与profile相关联的author:
App.Profile = DS.Model.extend({ about: DS.attr('string'), postCount: DS.attr('number'), author: DS.belongsTo('App.Author') }); App.Author = DS.Model.extend({ profile: DS.belongsTo('App.Profile'), name: DS.attr('string') });
Has Many 方法
hasMany方法描述了一对多的关系。在博客的例子中,一篇文章可能有多个评论:
App.Comment = DS.Model.extend({ content: DS.attr('string'), post: DS.belongsTo('App.Post') }); App.Post = DS.Model.extend({ content: DS.attr('string'), comments: DS.hasMany('App.Comment') });
关系表示
一对一
默认的REST适配器支持几种不同的关系变化以最佳的适应你的应用需求。我们将依次检验每种不同的关系类型。
让我们先讨论典型的一对一关系。我们将使用上面提到的Profile和Author的例子。在那个例子中,Profile属于一个Author,并且Author拥有一个Profile。最简单的方式去描述这两个记录的JSON对象结构如下:
// Author { "author": { "id": 1, "name": "Tom Dale" } } } // Profile { "profile": { "id": 1, "about": "Tom Dale is a software engineer that drinks too much beer.", "postCount": 1984, "author_id": 1 } }
注意上面的例子,Author的JSON中并未包含任何与Profile相关的信息。如果我们这样去请求对应的profile:
author.get('profile');
REST适配器会发送一个请求到一个URL:/profiles?author_id=1。(查询Profile的Author不会产生额外的请求,因为与Author对应的ID是内部的响应。)
作为性能优化,REST API会在返回的Author JSON中附带Profile的ID。
// Author with included Profile id { "author": { "id": 1, "name": "Tom Dale", "profile_id": 1 } }
现在,如果你请求author的profile,两件事中的一件会发生。如果带有ID的Profile已经在应用运行过程载入,它会立刻返回,不产生任何额外请求。否则,REST 适配器会发送请求到 /profile/1 去载入指定的profile。
在某些情况下,如果你知道你将总会使用到两个相关联的记录,你可以通过将记录包含在同一个JSON中来最小化HTTP请求。
一种方式是直接嵌入到父记录。比如,我们可以改写上面的例子:
{ "authors": [{ "id": 1, "name": "Tom Dale", "profile": { "id": 1, "about": "Tom Dale is a software engineer that drinks too much beer.", "postCount": 1984, "author_id": 1 } }] }
如果你这样做,注意到Profile.find(1)仍然会触发一个Ajax请求,直到你第一次访问嵌入的profile记录(Author.find(1).get('profile'))。
另一种方式是使用上面的格式(ID内嵌),然后侧载(sideloading)记录。例如:
{ "authors": [{ "id": 1, "name": "Tom Dale", "profile_id": 1 }], "profiles": [{ "id": 1, "about": "Tom Dale is a software engineer that drinks too much beer.", "postCount": 1984, "author_id": 1 }] }
然而,想象服务器返回一个Person的JSON对象:
{ "person": { "id": 1, "name": "Tom Dale", "tags": [{ "id": 1, "name": "good-looking" }, { "id": 2, "name": "not-too-bright" }] } }
这种情况下,它是一个嵌入对象的数组,而不是关联ID的数组。要让store正确的理解,需要设置embedded选项为true:
App.Person = DS.Model.extend({ tags: DS.hasMany('App.Tag', { embedded: true }) });
另外,也可以改变被映射到关联的数据属性。假设一个Person的JSON如下:
{ "person": { "id": 2, "name": "Carsten Nielsen", "tag_ids": [1, 2] } }
这种情况下你可以指定关联中的key属性:
App.Person = DS.Model.extend({ tags: DS.hasMany('App.Tag', { key: 'tag_ids' }) });
查找特定的记录实例
你可以通过唯一ID检索记录,使用find方法:
var model = App.store.find(App.Person, 1);
如果该记录已经加载,它会立刻返回。否则,一个空对象会返回。你可以设置绑定或观察属性到你感兴趣的属性上;一旦数据从持久层返回,所有指定的属性将自动更新。
除了方法find(),所有下面提到的方法都以类似的方式操作。
查询记录实例
你可以将一个对象作为第二个参数做为服务器查询。在这种情况下,你会得到一个ModelArray对象。
App.people = App.store.find(App.Person, { page: 1 });
起初,这个people数组不含有元素。过后,我们将看到适配器如何填充people数组。因为people是Ember数组,你可以立即把它插入到DOM里。当它被填充之后,Ember的绑定会自动更新DOM。
<ul>
{{#each App.people}}
<li>{{fullName}}</li>
{{/each}}
</ul>
这允许你请求store的一个数组信息,并保持你的视图代码与数组如何填充完全无关。
注:如果从ModelArray里人工检索记录,你需要使用objectAt(index)方法。由于对象不是一个JavaScript数组,使用[]符号将无法正常工作。
查找模型类型的所有记录
要找到某种类型的所有记录,使用store的findAll()方法:
var people = App.store.findAll(App.Person);
所有当前已经加载的与查找类型相关的记录会被包含在ModelArray并立刻返回。如果有必要,你的适配器也有机会加载该类型的额外的记录。
每当该类型的新的记录被加载进store时,由findAll方法返回的ModelArray将更新以反映新的数据。这意味着你可以将它传递给Ember模板中的#each助手,它会保持新数据的加载。
过滤已加载记录
你可以用store的filter()方法过滤任一模型类型的所有记录,使用一个函数来决定记录的去留。为了避免无谓的物化记录对象,只有从持久层返回的原始数据哈希被传递。
要将记录保留,让函数返回true。否则,返回false或undefined:
var oldPeople = App.store.filter(App.Person, function(data) { if (data.age > 80) { return true; } });
创建新记录
你可以用createRecord()方法,基于特定模型来创建新记录:
var wycats = App.store.createRecord(App.Person, { name: "Brohuda" });
新记录不会保存到持久层,直到调用了store的commit()方法。
更新记录
更新记录只需简单的改变他们的属性。更新的记录不会保存直到调用了store的commit()方法,这样你可以批量改变记录并一次性提交。
删除记录
调用deleteRecord()方法来删除记录:
var person = App.store.find(App.Person, 1); person.deleteRecord();
记录不会在持久层删除,直到调用了store的commit()方法。然后,删除的记录会立刻从它的ModelArray和关联中移除。
记录的生命周期
当记录的生命周期中某一事件发生时,可以调用函数。
didCreate - 当记录在持久层成功创建时调用;
didUpdate - 当记录在持久层成功更新时调用;
didLoad - 当数据从持久层完成加载时调用;
didDelete - 当记录从持久层中成功删除时调用。
例子:
App.Person = DS.Model.extend({ didLoad: function() { alert(this.get('firstName') + " finished loading."); } });
你也可以检查记录的状态属性,确定它的状态。
isLoaded - 当记录完成加载时为true,在本地创建的模型总是true;
isDirty - 当创建、更新、删除记录后但未保存时为true;
isSaving - 当记录处于保存进程时为true;
isDeleted - 记录已被删除为true,不论本地还是服务器;
isError - 当记录处于一个错误状态时为true;
加载数据
你可以预加载数据到store,当用户需要时即可使用。
加载单独一个记录,使用load()方法:
App.store.load(App.Person, { id: 1, firstName: "Peter", lastName: "Wagenet" });
加载多个记录,使用loadMany()方法:
App.store.loadMany(App.Person, [{ id: 2, firstName: "Erik", lastName: "Brynjolsofosonsosnson" }, { id: 3, firstName: "Yehuda", lastName: "Katz" }]);
Adapter API
适配器是一个对象,它接收来自store的请求,并将其转换成对持久层对应的行为。持久层通常是一个HTTP API,但可以是任何东西,如浏览器的本地存储。
创建适配器
首先,创建一个DS.Adapter的实例:
App.adapter = DS.Adapter.create();
设置adapter属性以告诉store使用哪个适配器:
App.store = DS.Store.create({ revision: 3, adapter: App.adapter });
然后,调用适配器需要的方法,方法描述于下文。
find()方法
调用find()方法,传入指定ID,来获取并填充一个记录。一旦记录被找到,调用store的load()方法:
App.Person = DS.Model.extend(); App.Person.reopenClass({ url: '/people/%@' }); DS.Adapter.create({ find: function(store, type, id) { var url = type.url; url = url.fmt(id); jQuery.getJSON(url, function(data) { // data is a Hash of key/value pairs. If your server returns a // root, simply do something like: // store.load(type, id, data.person) store.load(type, id, data); }); } });
当你调用store.find(type,id)时,store会调用你的适配器的find()方法。
注:本文档其余部分中,我们将使用我们适配器中的url属性。这不是写一个适配器的唯一方法。举例来说,你可以简单地把一个case语句放到每个方法中,每个类型做些不同的事情。或者你可以暴露适配器中使用的类型上不同的信息。我们简单地使用url来说明如何写一个适配器。
findMany()方法
调用findMany()方法,传入一组指定ID,来获取并填充所有记录。默认情况下,findMany()会重复的调用find(),但这会很低效。如果可以,你的服务器应该支持一种通过一组ID找到多个项目的方法。
一旦请求ID对应的数据准备好填充到store中时,调用store的loadMany()方法:
App.Person = DS.Model.extend(); App.Person.reopenClass({ url: '/people?ids=%@' }); DS.Adapter.create({ findMany: function(store, type, ids) { var url = type.url; url = url.fmt(ids.join(',')); jQuery.getJSON(url, function(data) { // data is an Array of Hashes in the same order as the original // Array of IDs. If your server returns a root, simply do something // like: // store.loadMany(type, ids, data.people) store.loadMany(type, ids, data); }); } });
在Rails中执行findMany[此部分暂不翻译]
It is extremely easy to implement an endpoint that will find many items in Ruby on Rails. Simply define the index action in a standard resourceful controller to understand an :ids parameter.
class PostsController < ApplicationController def index if ids = params[:ids] @posts = Post.where(:id => ids) else @posts = Post.scoped end respond_with @posts end end
findQuery()方法
当store的find()方法被一个查询调用时调用。你的适配器的findQuery()方法会传递一个ModelArray,你应当将其填充入从服务器返回的结果。
App.Person = DS.Model.extend(); App.Person.reopenClass({ collectionUrl: '/people' }); DS.Adapter.create({ findQuery: function(store, type, query, modelArray) { var url = type.collectionUrl; jQuery.getJSON(url, query, function(data) { // data is expected to be an Array of Hashes, in an order // determined by the server. This order may be specified in // the query, and will be reflected in the view. // // If your server returns a root, simply do something like: // modelArray.load(data.people) modelArray.load(data); }); } });
在你的适配器中,你可以做任意查询,但最常见的是将Ajax请求的数据部分发送到服务器。
你的服务器将负责返回一个JSON数据数组。当你将数据加载到modelArray,这个数组的元素将同时被加载到store里。
findAll()方法
当store的findAll()方法被调用时调用,如果你不做任何事,只有已加载的模型会被包含在结果中。否则,这是加载任何未加载的该类型的记录的机会。这个方法的实行与findMany()类似;查看上面的例子。
createRecord()方法
当store的commit()方法被调用,且有记录需要在服务器上创建时,store会调用适配器的creatRecord()方法。
一旦store调用适配器的creatRecord()方法,它将进入saving状态,此时试图编辑模型会导致错误。
调用creatRecord()方法很直接:
App.Person = DS.Model.extend(); App.Person.reopenClass({ url: '/people/%@' }); DS.Adapter.create({ createRecord: function(store, type, model) { var url = type.url; jQuery.ajax({ url: url.fmt(model.get('id')), data: model.get('data'), dataType: 'json', type: 'POST', success: function(data) { // data is a hash of key/value pairs representing the record. // In general, this hash will contain a new id, which the // store will now use to index the record. Future calls to // store.find(type, id) will find this record. store.didCreateRecord(model, data); } }); }) });
createRecords()方法
你可以在适配器上调用createRecords()方法以获得更好的效率,它会一次性发送所有新模型到服务器。
App.Person = DS.Model.extend(); App.Person.reopenClass({ collectionUrl: '/people' }); DS.Adapter.create({ createRecords: function(store, type, array) { jQuery.ajax({ url: type.collectionUrl, data: array.mapProperty('data'), dataType: 'json', type: 'POST', success: function(data) { // data is an array of hashes in the same order as // the original records that were sent. store.didCreateRecords(type, array, data); } }); }) });
updateRecord()方法
updateRecord()方法与createRecord()一样,除了在记录被保存后,需要调用store的didUpdateRecord()方法。
App.Person = DS.Model.extend(); App.Person.reopenClass({ url: '/people/%@' }); DS.Adapter.create({ updateRecord: function(store, type, model) { var url = type.url; jQuery.ajax({ url: url.fmt(model.get('id')), data: model.get('data'), dataType: 'json', type: 'PUT', success: function(data) { // data is a hash of key/value pairs representing the record // in its current state on the server. store.didUpdateRecord(model, data); } }); }) });
updateRecords()方法
再一次,updateRecords()与createRecords()类似
App.Person = DS.Model.extend(); App.Person.reopenClass({ collectionUrl: '/people' }); DS.Adapter.create({ updateRecords: function(store, type, array) { jQuery.ajax({ url: type.collectionUrl, data: array.mapProperty('data'), dataType: 'json', type: 'PUT', success: function(data) { // data is an array of hashes in the same order as // the original records that were sent. store.didUpdateRecords(array); } }); }) });
deleteRecord()方法
要删除记录,调用deleteRecord()方法,完成后调用store的didDeleteRecord()方法。
App.Person = DS.Model.extend(); App.Person.reopenClass({ url: '/people/%@' }); DS.Adapter.create({ deleteRecord: function(store, type, model) { var url = type.url; jQuery.ajax({ url: url.fmt(model.get('id')), dataType: 'json', type: 'DELETE', success: function() { store.didDeleteRecord(model); } }); }) });
deleteRecords()方法
同样的,你应该懂了吧?
App.Person = DS.Model.extend(); App.Person.reopenClass({ collectionUrl: '/people' }); DS.Adapter.create({ deleteRecords: function(store, type, array) { jQuery.ajax({ url: type.collectionUrl, data: array.mapProperty('data'), dataType: 'json', type: 'DELETE', success: function(data) { store.didDeleteRecords(array); } }); }) });
commit()方法
为了获得最大的效率,你可以将所有挂起的更改(创建,更新,删除)打包成一个迷你的数据包。要做到这一点,执行执行commit()方法,这将发送一切需要的信息到持久层。
这是默认的适配器的commit()方法:
commit: function(store, commitDetails) { commitDetails.updated.eachType(function(type, array) { this.updateRecords(store, type, array.slice()); }, this); commitDetails.created.eachType(function(type, array) { this.createRecords(store, type, array.slice()); }, this); commitDetails.deleted.eachType(function(type, array) { this.deleteRecords(store, type, array.slice()); }, this); }
连接到视图
Ember Data 总会立刻返回某一类型的记录或记录数组,即使底层的JSON对象还未从服务器返回。
一般情况下,这意味着,你可以用Ember的Handlebars模板引擎将它们插入到DOM中,当适配器填充完成后,它们会自动更新。
比如,你请求一个ModelArray:
App.people = App.store.find(App.Person, { firstName: "Tom" });
你会获得一个空的ModelArray。Ember Data会接着请求适配器用记录填充ModelArray,通常这会发起一个Ajax请求。然而,你可以立即在你的模板中引用它。
<ul>
{{#each App.people}}
<li>{{fullName}}</li>
{{/each}}
</ul>
一旦适配器调用了modelArray.load(array),DOM会自动用新的信息填充。
对记录本身也一样。例如,你可以请求单条记录:
App.person = App.store.find(App.Person, 1);
你会立刻获得一个未填充的Person对象。你可以立即在视图中引用它:
{{App.person.fullName}}
最初,这将是空的,但适配器调用store.load(hash)时,它会用所提供的信息自动更新。
如果你想在记录加载进程时显示不同的内容,可以使用record的isLoaded属性:
{{#with App.person}} {{#if isLoaded}} Hello, {{fullName}}! {{else}} Loading... {{/if}} {{/with}}
注:对于ModelArray来说也是一样的,同样使用isLoaded属性。
当记录保存时,你还可以向用户表面这一状态,比如:
{{#with App.person}} <h1 {{bindAttr class="isSaving"}}>{{fullName}}</h1> {{/with}}
这种情况下,你会得到名为is-saving的class,你可以在CSS里将它的内容改为灰色或增加一个不同的样式。