model是指数据层,也就是一个用于向用户展示的数据对象。考虑到前后端分离,其实可以认为展现内容与后端的DTO对象或者DVO对象是一一对应的。同时,model会通过json的形式以http请求的方式与服务器进行交互。但是,model的形式不一定是json的,其他的形式都是可以的。也就是说,model是持久化的。
这种方式下,ember本身提供了许多api,这也就是Ember data做的事情。Ember Data为你提供了更加简便的方式操作数据,统一管理数据的加载,降低程序复杂度。
Ember Data有几个重要的概念,分别为:models、records、adapters、store、serializer.
个人认为,model这个类,应该和后端的DO或者是DTO对象是一一对应的。 model的定义,可以通过命令行方式: ember g model <your-model-name> 则会创建一个app/models/<your-model-name>.js的文件。其中就是model的主体,由若干个属性构成,如:
export default DS.Model.extend({ title: DS.attr('string'), // 字符串类型 flag: DS.attr('boolean'), // 布尔类型 timestamp: DS.attr('number'), // 数字类型 birth: DS.attr('date'), //日期类型 addr: DS.attr(), //json类型,其实就是string });这里需要注意,没有id字段,Ember 会默认生成id属性。 如果有对应关系的话,比如一对多或者多对一的情况,那么可以使用Ember Data中的hasMany 以及 belongsTo方法。belongsTo为一对一关系,hasMany为一对多关系。使用方式如下
import DS from 'ember-data'; //app/models/blog-post.js export default DS.Model.extend({ comments: DS.hasMany('comment') }); //app/models/commont.js export default DS.Model.extend({ blogPost: DS.belongsTo('blog-post') });如果是多对多的关系,则如下就好:
import DS from 'ember-data'; //app/models/blog-post.js export default DS.Model.extend({ comments: DS.hasMany('comment') }); //app/models/commont.js export default DS.Model.extend({ blogPost: DS.hasMany('blog-post') });这个对应关系,可以看做为,实际的逻辑表之间的对应关系。
此外,还可以添加计算属性,因为,model也是一个Ember Object
export default DS.Model.extend({ title: DS.attr('string'), // 字符串类型 flag: DS.attr('boolean'), // 布尔类型 tf: Ember.computed('title', 'flag', function() { return `${this.get('title')} ${this.get('flag')}`; }) });store这个概念,在Ember Data中十分重要。 store是指所有的从服务端活动的record的数据以及新创建的record数据。即,store为所有数据的缓存,所有关于record的增删改查动作,都需要经过store。
一般情况下,一个应用,只有一个DS.store,它是由Ember.Application初始化的。
record是指一个model的实例,他可能是一个从服务端返回的数据,当然,你自己也可以新建一个record。
假设我们有一个route:
// app/routes/store-route.js import Ember from 'ember'; export default Ember.Route.extend({ model: function() { } });store 这个概念,可以在route以及controller中使用。形如this.get('store') 或者 this.store。
这里需要注意,DS.RecordArray,并不是一个[]它使一个可遍历的对象。这意味着,如果你在模板中,使用了这些reocrd,那么就可以享受到双向绑定的好处,同时,也提供了一些API,能够帮助你更方便的使用。具体可参考:Ember.js DS.Store
使用store.createRecord()来增加一个record。
model(){ let a = this.store.createRecord('model-example',{ firstName:'firstName', secondName: 'secondName' }); return a; }则返回一个record。
在运用场景上,我们可以设想这样一个场景,比如一个表单:
//route route-example.hbs <form class="" action="index.html" method="post"> firstName:{{input value=model.firstName}} secondName:{{input value=model.secondName}} <button type="submit" name="button" {{action 'submitForm'}}>提交</button> </form> //route route-example.js import Ember from 'ember'; export default Ember.Route.extend({ model(){ return this.store.createRecord('model-example',{ firstName:'', secondName: '' }); }, actions:{ actionSubmit(){ let model = this.modelFor('route-example'); model.save(); //这里会发出一个post请求,即 域名/model-example, 这里是localhost:8000/model-example,参数为{"data":{"attributes":{"first-name":"<你填写的内容>","second-name":"<你填写的内容>"},"type":"model-examples"}} } } });生成的页面如下:
这的数据是一个双向绑定的过程,在页面上的变动会直接变化到model上。
更新record的操作也是一样的
this.store.findRecord('model-example', 1).then(function(data) { data.set('firstName', "name1"); });这样就可以对其进行修改。
持久化,也就是说将数据存入数据库,实际上是一个与后端进行交互的过程。 调用sava()方法就可以。 但是不同于新建的post请求,所有更新操作的请求为PATCH请求。
this.store.findRecord('model-example', 1).then(function(data) { data.set('firstName', "name1"); data.save() //patch 请求:域名/model-examples/1, 在这里为:http://localhost:4200/model-examples/1 //参数 {"data":{"id":"1","attributes":{"firstName":"asdas","secondName":"sn"},"type":"model-examples"}} });使用deleteRecord()可以删除一个record
this.store.findRecord('model-example', 1).then(function(data) { data.deleteRecord(); data.get('isDeleted');//true =>表示已经删除 });如果想要持久化到后端,调用sava()
this.store.findRecord('model-example', 1).then(function(data) { data.deleteRecord(); data.get('isDeleted');//true =>表示已经删除 data.save();//这会发送一个DELETE请求,为 http://localhost:4200/model-examples/1 });或者直接调用
this.store.findRecord('model-example', 1).then(function(data) { data.destroyRecord();//这会发送一个DELETE请求,为 http://localhost:4200/model-examples/1 });ember还支持直接向store中插入record,但是这个过程不是与后端交互的过程。是通过push()方法实现的。
//route route-example.js export default Ember.Route.extend({ model() { this.get('store').push({ data: [{ id: 1, type: 'model-example', attributes: { firstName: 'fn1', secondName: 'sn1', }, }, { id: 1, type: 'model-example', attributes: { firstName: 'fn1', secondName: 'sn1', }, }] }); } });这样就可以手动向其中插入两条record。例如,当时不想用他提供的方式如post、patch、delete进行增删改数据,那么可以使用ajax先更新或者插入数据,然后在手动push进store。
总结一下增删改查的请求形式: //TODO
适配器,决定了数据如何持久化到后端。举例而言,请求的URL、REST API的header等。 创建适配器的命令,同样通过命令行就可以创建:ember g adapter <your-adapter-name>。这里注意,这里的名字必须和路由的是一致的,关于路由的规则,我们后面独立说。 以application这个路由为例,其适配器配置可能为:
//app/adapters/application.js import DS from 'ember-data'; export default DS.JSONAPIAdapter.extend({ host: 'http://xxx.com', namespace: 'api/v1', pathForType: function(type) { return Ember.String.underscore(type); }, headers: { 'API_KEY': 'secret key', 'ANOTHER_HEADER': 'Some header value' } });下面解释一下这几个常用规则的配置:
host,指域名namespace,命名空间,如上述的情况下,model名为model-example的新建请求为(忽略pathForType方法):http://xxx.com/api/v1/model-examplepathForType,修改model的适配方式,如果你希望都是下划线的方式,可以调用Ember.String.underscore()方法,就可以把驼峰或者中划线改为下划线,那么上述的情况下,新建的请求为:http://xxx.com/api/v1/model_exampleheaders,http的heads,这里是支持计算属性的,例如: export default DS.JSONAPIAdapter.extend({ session: Ember.inject.service('session'), headers: Ember.computed('session.authToken', function() { return { 'API_KEY': this.get('session.authToken'),//这里的headers取自session的authToken字段 'ANOTHER_HEADER': 'Some header value' }; }) });在适配器中,你还可以重写函数findRecord() findAll……等等:
export default DS.JSONAPIAdapter.extend({ findRecord(store, type, id){ return Ember.$.getJson(`${this.get('host')}/${this.get('namespace')}/${type}/${id}`); } });序列化器serializers主要负责用来格式化数据。这个在Ember Data中十分重要,因为本身model的数据格式十分蛋疼,用起来非常难受。
创建serializers,也可以是命令行工具:ember g serializer <your-serializer-name>
Ember Data提供了3种serializers的默认实现,分别为:
JSONAPISerializer,2.0以后版本的默认实现JSONSerializer 针对单个json或者json array的简单实现RESTSerializer2.0以前版本的默认实现,较为复杂,可以支持边缘加载,即是通过增加数据层级来分步加载(可以理解为先加载第一层次的数据集合,然后按需加载子一级的数据集合)这意味着,根据自己的需求,继承其中任意一个类就可以。
现在考虑标准格式的record:
//单条record { "data": { "type": "people", "id": "123", "attributes": { "first-name": "Jeff", "last-name": "Atwood" } } } //recordArray { "data": [{ "type": "model-example", "id": "1", "attributes": { "firstName": "Jeff", "secondName": "Atwood" } }, { "type": "model-example", "id": "2", "attributes": { "firstName": "Yehuda", "secondName": "Katz" } }] } //如果数据除了本身之外还有关系数据,那么关系数据就放在`included`中 { "data": { "type": "articles", "id": "1", "attributes": { "title": "JSON API paints my bikeshed!" }, "links": { "self": "http://example.com/articles/1" }, "relationships": { "comments": { "data": [ { "type": "comments", "id": "5" }, { "type": "comments", "id": "12" } ] } } }, "included": [{ "type": "comments", "id": "5", "attributes": { "body": "First!" }, "links": { "self": "http://example.com/comments/5" } }, { "type": "comments", "id": "12", "attributes": { "body": "I like XML better" }, "links": { "self": "http://example.com/comments/12" } }] }我们从简单的情况入手,比如,正常情况下,我们的后端返回的数据格式可能为:
{ "result":[ { "id": 1, "firstName": "Jeff", "secondName": "Atwood" }, { "id": 1, "firstName": "Yehuda", "secondName": "Katz" } ] }这种情况下,如果返回的话,前端一定会报错,因为格式不匹配,找不到data。
这样,就可以将返回的result转化为data。
反之,如果请求中,后端需要的内容与前端给的不一样,只需要重写serialize方法:
import DS from 'ember-data'; export default DS.JSONSerializer.extend({ serialize: function(snapshot, options) { var json = this._super(...arguments); // ?? json.data.attributes.cost = { amount: json.data.attributes.amount, currency: json.data.attributes.currency }; delete json.data.attributes.amount; delete json.data.attributes.currency; return json; } });通过上述的方式,可以把前端发送的内容:
{ "data": { "attributes": { "id": "1", "name": "My Product", "amount": 100, "currency": "SEK" }, "type": "product" } }变为:
{ "data": { "attributes": { "id": "1", "name": "My Product", "cost": { "amount": 100, "currency": "SEK" } }, "type": "product" } }最后说两句,RESTSerializer的实现与其他两个不同,他没有规定返回的格式,也就是说可以是任意格式的,而其他的都是json。
