英文原版:https://guides.emberjs.com/v2.14.0/models/customizing-serializers/
在Ember Data中,序列化器用来给发送和接收的数据做格式化。默认的,Ember Data会以JSON API的形式来序列化数据。如果你的后端使用了不同的数据格式,Ember Data则允许你自定义序列化器或者使用符合要求的其他序列化器。
Ember Data附带了3个序列化器。JSONAPISerializer是默认的序列化器,它与符合JSON API要求的后端一起配合;JSONSerializer是一个比较简单的序列化器,如果你的数据是一个结构简单的json对象或记录数组,那么可以考虑用它;RESTSerializer是一个比较复杂序列化器,它支持侧向加载并且在版本2.0之前它是默认的序列化器。
JSONAPISerializer 约定
当请求一条记录时,JSONAPISerializer会期望从服务端返回的JSON遵从下列约定:
JSON API 文档
JSONAPISerializer期望后端返回符合JSON API规范和约定的JSON API 文档,示例参见:http://jsonapi.org/format。这意味着所有的type名称都应该为复数,并且属性名和关联关系名都应该以被破折号分割的单词命名。例子,如果你从/people/123请求一条记录,响应数据应该是这样:
{
"data": {
"type": "people",
"id": "123",
"attributes": {
"first-name": "Jeff",
"last-name": "Atwood"
}
}
}
当响应含有多条记录的时候,data属性的值应该是一个数组:
{
"data": [{
"type": "people",
"id": "123",
"attributes": {
"first-name": "Jeff",
"last-name": "Atwood"
}
}, {
"type": "people",
"id": "124",
"attributes": {
"first-name": "Yehuda",
"last-name": "Katz"
}
}]
}
侧向加载数据
对于不是本次请求的主要目标数据,但是由于包含关联关系链接,所以这些数据以数组的形式被包含在included键下。例子,如果你请求的是/articles/1,并且后端返回了与该文章相关的所有评论,那么响应数据看起来应该是下面这样:
{
"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"
}
}]
}
自定义序列化器
Ember Data默认使用JSONAPISerializer作为序列化器,不过你可以通过自定义序列化起来覆写它。有两种方式可以自定义序列化器。第一、你可以自定义一个application序列化器:
app/serializers/application.js
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({});
第二、你为某个模型来定义序列化器。例子,如果你有个post模型,那么你可以定义一个post序列化器:
app/serializers/post.js
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({});
为了改变发送给后端的数据的结构,你可以使用serialize() 钩子函数。我们现在从Ember Data那得到了一个JSON API格式的响应数据:
{
"data": {
"id": "1",
"type": "product",
"attributes": {
"name": "My Product",
"amount": 100,
"currency": "SEK"
}
}
}
但是,服务器所希望的数据格式是这样的:
{
"data": {
"id": "1",
"type": "product",
"attributes": {
"name": "My Product",
"cost": {
"amount": 100,
"currency": "SEK"
}
}
}
}
下面的代码演示了如何改变数据:
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
serialize(snapshot, options) {
let 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;
},
});
相似的,如果你的后端提供的数据格式是非JSON API格式的,那么你可以使用normalizeResponse() 钩子。我们还是用上面例子,服务端提供的数据是这样:
{
"data": {
"id": "1",
"type": "product",
"attributes": {
"name": "My Product",
"cost": {
"amount": 100,
"currency": "SEK"
}
}
}
}
我们需要的数据格式是这样:
{
"data": {
"id": "1",
"type": "product",
"attributes": {
"name": "My Product",
"amount": 100,
"currency": "SEK"
}
}
}
那么,我们可以这么干:
app/serializers/application.js
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
payload.data.attributes.amount = payload.data.attributes.cost.amount;
payload.data.attributes.currency = payload.data.attributes.cost.currency;
delete payload.data.attributes.cost;
return this._super(...arguments);
},
});
如果需要规范化的模型结构比较简单,用 normalize()钩子就可以了。
想要了解更多自定义序列化器,请参阅 Ember Data serializer API documentation.
ID属性
为了持续跟踪store中的记录,Ember Data希望每一条记录都带有一个id属性。id属性对于每一条记录来说应该是唯一的。如果你的后端用了另一个键来表示id,那么当你在序列化/反序列化数据时你可以通过序列化器的primaryKey 属性来将这个键转换为id。
app/serializers/application.js
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
primaryKey: '_id'
});
属性名
Ember Data约定模型属性名使用驼峰形式:
app/models/person.js
import DS from 'ember-data';
export default DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
isPersonOfTheYear: DS.attr('boolean')
});
然而,JSONAPISerializer 规定的属性名格式为破折号分割的形式:
{
"data": {
"id": "44",
"type": "people",
"attributes": {
"first-name": "Zaphod",
"last-name": "Beeblebrox",
"is-person-of-the-year": true
}
}
}
如果后端返回的数据的属性名格式使用了另一种约定形式,那么你可以使用序列化器的keyForAttribute()方法将模型中的属性名转换为与后台返回数据属性名格式一致的属性名。例子,假设后端返回数据中包含名为under_scored 的键,那么你可以向下面这样覆写keyForAttribute()方法:
app/serializers/application.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
keyForAttribute(attr) {
return Ember.String.underscore(attr);
}
});
不规则的键可以通过自定义序列化器来映射。attrs对象可以在DS.Model记录和负载数据的键之间建立映射关系。
如果表示person的JSON数据带有lastNameOfPerson键,并且对于模型这个属性叫lastName,那么就可以创建一个自定义序列化器并且覆写它的attrs 属性。
app/models/person.js
import DS from 'ember-data';
export default DS.Model.extend({
lastName: DS.attr('string')
});
app/serializers/person.js
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
attrs: {
lastName: 'lastNameOfPerson'
}
});
关联关系
通过ID来关联其他的记录。例子,假设现在有个带有hasMany关系的模型:
app/models/post.js
import DS from 'ember-data';
export default DS.Model.extend({
comments: DS.hasMany('comment', { async: true })
});
JSON应该将关系格式化为带有type和id的数组:
{
"data": {
"type": "posts",
"id": "1",
"relationships": {
"comments": {
"data": [
{ "type": "comments", "id": "1" },
{ "type": "comments", "id": "2" },
{ "type": "comments", "id": "3" }
]
}
}
}
}
附属于post的comments可以通过post.get(‘comments’)被读取。JSON API适配器将会发送3个GET请求分别获取/comments/1/,/comments/2/,/comments/3/。
任何belongsTo关系,通过JSON表示时键名都应该是以破折号分隔的形式。例子,假设现在有个模型:
app/models/comment.js
import DS from 'ember-data';
export default DS.Model.extend({
originalPost: DS.belongsTo('post')
});
JSON应该将关系格式化为通过ID指向另一条记录:
{
"data": {
"type": "comment",
"id": "1",
"relationships": {
"original-post": {
"data": { "type": "post", "id": "5" },
}
}
}
}
如果需要转换命名,可以通过实现keyForRelationship()方法来覆写。
app/serializers/application.js
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
keyForRelationship(key, relationship) {
return key + 'Ids';
}
});
创建自定义转换格式
在某些情况下,内建的属性类型:string, number, boolean, and date不能满足需要。例如,后端返回了一个非标准的数据格式。
Ember Data可以注册新的转换规则:
app/transforms/coordinate-point.js
import DS from 'ember-data';
export default DS.Transform.extend({
serialize(value) {
return [value.get('x'), value.get('y')];
},
deserialize(value) {
return Ember.Object.create({ x: value[0], y: value[1] });
}
});
app/models/cursor.js
import DS from 'ember-data';
export default DS.Model.extend({
position: DS.attr('coordinate-point')
});
当coordinatePoint 从接口获取到后,它会是一个数组:
{
cursor: {
position: [4,9]
}
}
但是从模型实例中获取它时,仍然通过对象的形式:
let cursor = store.findRecord('cursor', 1);
cursor.get('position.x'); //=> 4
cursor.get('position.y'); //=> 9
如果postion被编辑过然后被保存了,那么它会被传入transform的serialize( )方法,并且被重新转换为数组的形式。
JSONSerializer
并不是所有的API遵循JSONAPISerializer使用的约定,它们使用数据命名空间和侧负载关系记录。 一些传统的API可能返回一个简单的JSON有效载荷,这只是所请求的资源或一系列序列化的记录。 JSONSerializer是与Ember Data一起提供的序列化程序,可以与RESTAdapter一起使用以序列化这些更简单的API。
通过继承JSONSerializer来使用它:
app/serializers/application.js
import DS from 'ember-data';
export default DS.JSONSerializer.extend({
// ...
});
对于仅返回1条记录的请求(比如,store.findRecord(‘post’, 1) )。JSONSerializer 所期望的返回数据的格式是下面这样:
{
"id": "1",
"title": "Rails is omakase",
"tag": "rails",
"comments": ["1", "2"]
}
对于返回多条记录的请求(比如:store.findAll(‘post’) 、 store.query(‘post’, { filter: { status: ‘draft’ } } ) )。JSONSerializer 期望的响应数据格式为下面这样:
[{
"id": "1",
"title": "Rails is omakase",
"tag": "rails",
"comments": ["1", "2"]
}, {
"id": "2",
"title": "I'm Running to Reform the W3C's Tag",
"tag": "w3c",
"comments": ["3"]
}]
JSONAPISerializer构建在JSONSerializer之上,因此它们共享许多相同的钩子来自定义序列化过程的行为。 请务必查看API文档以获取完整的方法和属性列表。
EmbeddedRecordMixin
虽然Ember Data鼓励通过侧向加载的方法来加载关联关系,但是当你要处理传统的接口时,你会发现你需要处理嵌在某条记录中的关联关系的JSON数据。EmbeddedRecordMixin就是帮你解决此问题。
要设置嵌入式记录,请在扩展序列化程序时包含mixin,然后定义和配置嵌入式关系。
例子,假设现在有个post模型,它被嵌入了author记录:
{
"id": "1",
"title": "Rails is omakase",
"tag": "rails",
"authors": [
{
"id": "2",
"name": "Steve"
}
]
}
你需要这样定义关联关系:
app/serializers/post.js
import DS from 'ember-data';
export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
authors: {
serialize: 'records',
deserialize: 'records'
}
}
});
如果对于嵌入的关系既要序列化,又需要对其反序列化,那么你可以通过短设置{ embedded: ‘always’ }来配置此行为。上面的例子可以改写为:
app/serializers/post.js
import DS from 'ember-data';
export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
authors: { embedded: 'always' }
}
});
serialize 和deserialize支持3个值:
- records 表示期望一个完整的记录
- ids 表示只需要记录的id
- 列表内容
false 表示不包含该记录
例如,您可能会发现,在提取JSON有效内容时,您需要读取嵌入的记录,但只能在序列化记录时包含关系的ID。 这可以通过使用serialize:’ids’选项。 您还可以通过设置serialize:false来选择不序列化关系。
EmbeddedRecordsMixin 默认值
如果你没有覆写attrs ,EmbeddedRecordsMixin的默认行为如下:
- BelongsTo: { serialize: ‘id’, deserialize: ‘id’ }
- HasMany: { serialize: false, deserialize: ‘ids’ }
编写序列化器
如果你想要创建自定义序列化器,那么建议优先继承 JSONAPISerializer 或 JSONSerializer。然后如果你的需求是在跟JSONAPISerializer 和 JSONSerializer差距太大,那么可以继承DS.Serializer这个基础类。
序列化器用来将从适配器获得的数据负载规范化为Ember Data能理解格式。它同样也用来将一段记录转换为adapter要发送给后端的数据的格式。
Ember Data的规范化JSON格式
Ember Data期望的规范化JSON格式是一个带有额外限制的JSON API 文档
首先,要保证的是在规范化的JSON对象中的type名称要与该类型的模型文件的名称要绝对一致。依据管理,模型名在Ember Data中是单数的,然而,JSON API规范中提供的示例却是复数的。Ember Data的JSONAPISerializer 已经假定type是复数,它会自动将类型单数化。
其次,在JSON API文档中属性和关联关系的命名必须与Model中属性名称和DS.attr(), DS.belongsTo() , DS.hasMany()等条件相匹配。
依据惯例,这些属性名在Ember Data中是驼峰形式的。与type名一样,这也与JSON API 规范提供的例子是不符的。规范中使用的是破折号为分隔的属性名和关联关系名。然而,规范却并没有要求属性名和关联关系名必须遵守某种约定。如果你使用JSONAPISerializer ,那么它会假定属性名和关系名是破折号分隔的,并且会自动将它转化为驼峰形式。
除了以上两条限制之外,Ember Data的规范化JSON对象遵循JSON API 规范。
例子,现在有个post模型:
app/models/post.js
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
tag: DS.attr('string'),
comments: hasMany('comment', { async: false }),
relatedPosts: hasMany('post')
});
Ember Data期望序列化器返回的规范化JSON对象是下面这种结构:
{
data: {
id: "1",
type: "post",
attributes: {
title: "Rails is omakase",
tag: "rails",
},
relationships: {
comments: {
data: [{ id: "1", type: 'comment' },
{ id: "2", type: 'comment' }],
},
relatedPosts: {
links: {
related: "/api/v1/posts/1/related-posts/"
}
}
}
}
需要注意的是,类型post匹配post模型,relatedPosts关系与模型中的relatedPosts: hasMany(‘post’)匹配。
规范化适配器响应数据
当你创建自定义序列化器时,需要定义normalizeResponse方法,用来将从适配器的返回数据转换为上面的代码描述的规范化的JSON对象。
这个方法接收store,所请求的Model类,数据负载,被请求记录的id和请求类型(‘findRecord’, ‘queryRecord’, ‘findAll’, ‘findBelongsTo’, ‘findHasMany’, ‘findMany’, ‘query’, ‘createRecord’, ‘deleteRecord’, 和’updateRecord’)为参数。
自定义序列化器也需要定义normalize 方法。这个方法通过store.normalize(type, payload)被调用,它经常被用来规范位于Ember Data之外的请求,应该它没有被归纳入适配器的增删改查操作过程。
序列化记录
最后,序列化器需要实现serialize 方法。Ember Data会提供一个记录片段和一个hash格式的选项,并且这个方法返回的对象会被适配器在创建,更新和删除记录时被发送到后端。
Community Serializers
如果内建的序列化器不能满足你的需要,那么一个不错的寻找他们的地方 –> Ember Observer
本章完