[译]JSON数据范式化(normalizr)
开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同数据相互引用时通过 ID 来查找。把 应用的 state 想像成数据库 。这种方法在 normalizr 文档里有详细阐述。
normalizr:将嵌套的JSON格式扁平化,方便被Redux利用;
目标
我们的目标是将:
数组的每个对象都糅合了三个维度 文章、 作者
按照数据范式,应当将这两个维度拆分出来,两者的联系通过id关联起来即可
我们描述上述的结构: - 返回的是一个数组(array) - 数组的对象中包含另外一个schema user
应该比较合理的,应该是转换成:
如何使用
先引入 normalizr:
定义schema:
定义规则:
测试:
input ={ feed:[{ id:1, title:'Some Article', author:{ id:3, name:'Mike Persson'}, collections:[{ id:1, title:'Awesome Writing', curator:{ id:4, name:'Andy Warhol'}},{ id:7, title:'Even Awesomer', curator:{ id:100, name:'T.S. Eliot'}}]},{ id:2, title:'Other Article', collections:[{ id:2, title:'Neverhood', curator:{ id:120, name:'Ada Lovelace'}}], author:{ id:2, name:'Pete Hunt'}}]}; Object.freeze(input);normalize(input, feedSchema).should.eql({ result:{ feed:[1,2]}, entities:{ articles:{1:{ id:1, title:'Some Article', author:3, collections:[1,7]},2:{ id:2, title:'Other Article', author:2, collections:[2]}}, collections:{1:{ id:1, title:'Awesome Writing', curator:4},2:{ id:2, title:'Neverhood', curator:120},7:{ id:7, title:'Even Awesomer', curator:100}}, users:{2:{ id:2, name:'Pete Hunt'},3:{ id:3, name:'Mike Persson'},4:{ id:4, name:'Andy Warhol'},100:{ id:100, name:'T.S. Eliot'},120:{ id:120, name:'Ada Lovelace'}}}});
优势
假定请求 /articles 返回的数据的schema如下:
articles: article* article: { author: user, likers: user* primary_collection: collection? collections: collection* } collection: { curator: user }
如果不做范式化,store需要事 知道API的各种结构,比如UserStore会包含很多样板代码来获取新用户,诸如下面那样:
switch(action.type){case ActionTypes.RECEIVE_USERS:mergeUsers(action.rawUsers);break;case ActionTypes.RECEIVE_ARTICLES: action.rawArticles.forEach(rawArticle =>{mergeUsers([rawArticle.user]);mergeUsers(rawArticle.likers);mergeUsers([rawArticle.primaryCollection.curator]); rawArticle.collections.forEach(rawCollection =>{mergeUsers(rawCollection.curator);});}); UserStore.emitChange();break;}
store表示累觉不爱啊!! 每个store都要对返回的 进行各种foreach 才能获取想要的数据。
来一个范式吧:
const article =newSchema('articles');const user =newSchema('users'); article.define({ author: user, contributors:arrayOf(user), meta:{ likes:arrayOf({ user: user })}});// ...const json =getArticleArray();const normalized =normalize(json,arrayOf(article));
经过范式整顿之后,你爱理或者不爱理,users对象总是在 action.entities.users 中:
const{ action }= payload;if(action.response && action.response.entities && action.response.entities.users){mergeUsers(action.response.entities.users); UserStore.emitChange();break;}
更多示例(来自测试文件)
规范化单个文件
var article =newSchema('articles'), input; input ={ id:1, title:'Some Article', isFavorite:false}; Object.freeze(input);normalize(input, article).should.eql({ result:1, entities:{ articles:{1:{ id:1, title:'Some Article', isFavorite:false}}}});
规范化内嵌对象,并删除额外key
有时候后端接口会返回很多额外的字段,甚至会有重复的字段;比如下方示例中 typeId 和type.id 是重复的;注意方法中 形参key 是经过artcle.define 定义过的。
var article =newSchema('articles'), type =newSchema('types'), input;// 定义内嵌规则 article.define({ type: type }); input ={ id:1, title:'Some Article', isFavorite:false, typeId:1, type:{ id:1,}}; Object.freeze(input);// assignEntity删除后端返回额外数据的var options ={ assignEntity:function(obj, key, val){ obj[key]= val;delete obj[key +'Id'];}};normalize(input, article, options).should.eql({ result:1, entities:{ articles:{1:{ id:1, title:'Some Article', isFavorite:false, type:1}}, types:{1:{ id:1}}}});
添加额外数据
和上个示例相反的是,mergeIntoEntity 用于将多份同质数据不同信息融合到一起,用于解决冲突。
下方示例中,author 和reviewer 是同一个人,只是前者留下的联系方式是手机,后者留下的联系方式是邮箱,但无论如何都是同一个人;
此时就可以使用 mergeIntoEntity 将两份数据融合到一起;(注意这里是用 valueOf规则)
var author =newSchema('authors'), input; input ={ author:{ id:1, name:'Ada Lovelace', contact:{ phone:'555-0100'}}, reviewer:{ id:1, name:'Ada Lovelace', contact:{ email:'ada@lovelace.com'}}} Object.freeze(input);var options ={ mergeIntoEntity:function(entityA, entityB, entityKey){var key;for(key in entityB){if(!entityB.hasOwnProperty(key)){continue;}if(!entityA.hasOwnProperty(key)||isEqual(entityA[key], entityB[key])){ entityA[key]= entityB[key];continue;}if(isObject(entityA[key])&&isObject(entityB[key])){merge(entityA[key], entityB[key])continue;} console.warn('Unequal data!');}}};normalize(input,valuesOf(author), options).should.eql({ result:{ author:1, reviewer:1}, entities:{ authors:{1:{ id:1, name:'Ada Lovelace', contact:{ phone:'555-0100', email:'ada@lovelace.com'}}}}});
按指定的属性规范化
有时候对象没有 id 属性,或者我们并不想按 id 属性规范化, 以使用 idAttribute 指定;
下面的例子,就是使用slug作为规范化的key:
var article =newSchema('articles',{ idAttribute:'slug'}), input; input ={ id:1, slug:'some-article', title:'Some Article', isFavorite:false}; Object.freeze(input);normalize(input, article).should.eql({ result:'some-article', entities:{ articles:{'some-article':{ id:1, slug:'some-article', title:'Some Article', isFavorite:false}}}});
创建自定义的属性
有时候想自己创建一个key,虽然今天和去年创建的文章名称都是Happy,但明显是不一样的,为了按时间区分出来,可以 使用自定义函数生成想要的key。
functionmakeSlug(article){var posted = article.posted, title = article.title.toLowerCase().replace(' ','-');return[title, posted.year, posted.month, posted.day].join('-');}var article =newSchema('articles',{ idAttribute: makeSlug }), input; input ={ id:1, title:'Some Article', isFavorite:false, posted:{ day:12, month:3, year:1983}}; Object.freeze(input);normalize(input, article).should.eql({ result:'some-article-1983-3-12', entities:{ articles:{'some-article-1983-3-12':{ id:1, title:'Some Article', isFavorite:false, posted:{ day:12, month:3, year:1983}}}}});
规范化数组
后端返回的数据往往是一串数组居多,此时规范化起到很大的作用,规范化的同时将数据压缩了一遍;
var article =newSchema('articles'), input; input =[{ id:1, title:'Some Article'},{ id:2, title:'Other Article'}]; Object.freeze(input);normalize(input,arrayOf(article)).should.eql({ result:[1,2], entities:{ articles:{1:{ id:1, title:'Some Article'},2:{ id:2, title:'Other Article'}}}});
抽取多个schema
上面讲的情形比较简单,只涉及抽出结果是单个schema的情形;现实中,你往往想抽象出多个schema,比如下方,我想抽离出 tutorials(教程) 和articles(文章)两个 schema,此时需要 通过schemaAttribute 选项指定区分这两个 schema 的字段:
var article =newSchema('articles'), tutorial =newSchema('tutorials'), articleOrTutorial ={ articles: article, tutorials: tutorial }, input; input =[{ id:1, type:'articles', title:'Some Article'},{ id:1, type:'tutorials', title:'Some Tutorial'}]; Object.freeze(input);normalize(input,arrayOf(articleOrTutorial,{ schemaAttribute:'type'})).should.eql({ result:[{id:1, schema:'articles'},{id:1, schema:'tutorials'}], entities:{ articles:{1:{ id:1, type:'articles', title:'Some Article'}}, tutorials:{1:{ id:1, type:'tutorials', title:'Some Tutorial'}}}});
这个示例中,虽然文章的id都是1,但很明显它们是不同的文章,因为一篇是普通文章,一篇是教程文章;因此要按schema维度抽离数据;
这里的 arrayOf(articleOrTutorial) 中的articleOrTutorial 是包含多个属性的对象,这表示 input 应该是 articleOrTutorial 中的一种情况;
有时候原始数据属性 和 我们定义的有些差别,此时可以将 schemaAttribute 的值设成函数,将原始属性经过适当加工;比如原始属性是tutorial , 而抽离出的 schema 名字为 tutorials ,相差一个s:
functionguessSchema(item){return item.type +'s';}var article =newSchema('articles'), tutorial =newSchema('tutorials'), articleOrTutorial ={ articles: article, tutorials: tutorial }, input; input =[{ id:1, type:'article', title:'Some Article'},{ id:1, type:'tutorial', title:'Some Tutorial'}]; Object.freeze(input);normalize(input,arrayOf(articleOrTutorial,{ schemaAttribute: guessSchema })).should.eql({ result:[{ id:1, schema:'articles'},{ id:1, schema:'tutorials'}], entities:{ articles:{1:{ id:1, type:'article', title:'Some Article'}}, tutorials:{1:{ id:1, type:'tutorial', title:'Some Tutorial'}}}});
上述是数组情况,针对普通的对象也是可以的,将规则 改成 valueOf 即可:
var article =newSchema('articles'), tutorial =newSchema('tutorials'), articleOrTutorial ={ articles: article, tutorials: tutorial }, input; input ={ one:{ id:1, type:'articles', title:'Some Article'}, two:{ id:2, type:'articles', title:'Another Article'}, three:{ id:1, type:'tutorials', title:'Some Tutorial'}}; Object.freeze(input);normalize(input,valuesOf(articleOrTutorial,{ schemaAttribute:'type'})).should.eql({ result:{ one:{id:1, schema:'articles'}, two:{id:2, schema:'articles'}, three:{id:1, schema:'tutorials'}}, entities:{ articles:{1:{ id:1, type:'articles', title:'Some Article'},2:{ id:2, type:'articles', title:'Another Article'}}, tutorials:{1:{ id:1, type:'tutorials', title:'Some Tutorial'}}}});
schemaAttribute 是函数的情况就不列举了,和上述一致;
规范化内嵌情形
上面的对象比较简单,原本就是扁平化的;如果对象格式稍微复杂一些,比如每篇文章有多个作者的情形。此时需要使用 define 事先声明 schema 之间的层级关系:
var article =newSchema('articles'), user =newSchema('users'), input; article.define({ author: user }); input ={ id:1, title:'Some Article', author:{ id:3, name:'Mike Persson'}}; Object.freeze(input);normalize(input, article).should.eql({ result:1, entities:{ articles:{1:{ id:1, title:'Some Article', author:3}}, users:{3:{ id:3, name:'Mike Persson'}}}});
上面是不是觉得简单了?那么给你一个比较复杂的情形,万变不离其宗。我们最终想抽离出 articles、users 以及collections 这三个 schema,所以只要定义这三个schema就行了,
然后使用 define 方法声明这三个schema之间千丝万缕的关系;
最外层的feed只是属性,并不需要定义;
var article =newSchema('articles'), user =newSchema('users'), collection =newSchema('collections'), feedSchema, input; article.define({ author: user, collections:arrayOf(collection)}); collection.define({ curator: user }); feedSchema ={ feed:arrayOf(article)}; input ={ feed:[{ id:1, title:'Some Article', author:{ id:3, name:'Mike Persson'}, collections:[{ id:1, title:'Awesome Writing', curator:{ id:4, name:'Andy Warhol'}},{ id:7, title:'Even Awesomer', curator:{ id:100, name:'T.S. Eliot'}}]},{ id:2, title:'Other Article', collections:[{ id:2, title:'Neverhood', curator:{ id:120, name:'Ada Lovelace'}}], author:{ id:2, name:'Pete Hunt'}}]}; Object.freeze(input);normalize(input, feedSchema).should.eql({ result:{ feed:[1,2]}, entities:{ articles:{1:{ id:1, title:'Some Article', author:3, collections:[1,7]},2:{ id:2, title:'Other Article', author:2, collections:[2]}}, collections:{1:{ id:1, title:'Awesome Writing', curator:4},2:{ id:2, title:'Neverhood', curator:120},7:{ id:7, title:'Even Awesomer', curator:100}}, users:{2:{ id:2, name:'Pete Hunt'},3:{ id:3, name:'Mike Persson'},4:{ id:4, name:'Andy Warhol'},100:{ id:100, name:'T.S. Eliot'},120:{ id:120, name:'Ada Lovelace'}}}});
内嵌+数组倾斜
var article =newSchema('articles'), tutorial =newSchema('tutorials'), articleOrTutorial ={ articles: article, tutorials: tutorial }, user =newSchema('users'), collection =newSchema('collections'), feedSchema, input; article.define({ author: user, collections:arrayOf(collection)}); tutorial.define({ author: user, collections:arrayOf(collection)}); collection.define({ curator: user }); feedSchema ={ feed:arrayOf(articleOrTutorial,{ schemaAttribute:'type'})}; input ={ feed:[{ id:1, type:'articles', title:'Some Article', author:{ id:3, name:'Mike Persson'}, collections:[{ id:1, title:'Awesome Writing', curator:{ id:4, name:'Andy Warhol'}},{ id:7, title:'Even Awesomer', curator:{ id:100, name:'T.S. Eliot'}}]},{ id:1, type:'tutorials', title:'Some Tutorial', collections:[{ id:2, title:'Neverhood', curator:{ id:120, name:'Ada Lovelace'}}], author:{ id:2, name:'Pete Hunt'}}]}; Object.freeze(input);normalize(input, feedSchema).should.eql({ result:{ feed:[{ id:1, schema:'articles'},{ id:1, schema:'tutorials'}]}, entities:{ articles:{1:{ id:1, type:'articles', title:'Some Article', author:3, collections:[1,7]}}, tutorials:{1:{ id:1, type:'tutorials', title:'Some Tutorial', author:2, collections:[2]}}, collections:{1:{ id:1, title:'Awesome Writing', curator:4},2:{ id:2, title:'Neverhood', curator:120},7:{ id:7, title:'Even Awesomer', curator:100}}, users:{2:{ id:2, name:'Pete Hunt'},3:{ id:3, name:'Mike Persson'},4:{ id:4, name:'Andy Warhol'},100:{ id:100, name:'T.S. Eliot'},120:{ id:120, name:'Ada Lovelace'}}}});
内嵌+ 对象(再内嵌)
看到下面的 valuesOf(arrayOf(user)) 了没有,它表示该属性是一个对象,对象里面各个数组值是 User对象数组;
var article =newSchema('articles'), user =newSchema('users'), feedSchema, input; article.define({ collaborators:valuesOf(arrayOf(user))}); feedSchema ={ feed:arrayOf(article), suggestions:valuesOf(arrayOf(article))}; input ={ feed:[{ id:1, title:'Some Article', collaborators:{ authors:[{ id:3, name:'Mike Persson'}], reviewers:[{ id:2, name:'Pete Hunt'}]}},{ id:2, title:'Other Article', collaborators:{ authors:[{ id:2, name:'Pete Hunt'}]}},{ id:3, title:'Last Article'}], suggestions:{1:[{ id:2, title:'Other Article', collaborators:{ authors:[{ id:2, name:'Pete Hunt'}]}},{ id:3, title:'Last Article'}]}}; Object.freeze(input);normalize(input, feedSchema).should.eql({ result:{ feed:[1,2,3], suggestions:{1:[2,3]}}, entities:{ articles:{1:{ id:1, title:'Some Article', collaborators:{ authors:[3], reviewers:[2]}},2:{ id:2, title:'Other Article', collaborators:{ authors:[2]}},3:{ id:3, title:'Last Article'}}, users:{2:{ id:2, name:'Pete Hunt'},3:{ id:3, name:'Mike Persson'}}}});
还有更加复杂的,这次用上 valuesOf(userOrGroup, { schemaAttribute: 'type' }) 了:
var article =newSchema('articles'), user =newSchema('users'), group =newSchema('groups'), userOrGroup ={ users: user, groups: group }, feedSchema, input; article.define({ collaborators:valuesOf(userOrGroup,{ schemaAttribute:'type'})}); feedSchema ={ feed:arrayOf(article), suggestions:valuesOf(arrayOf(article))}; input ={ feed:[{ id:1, title:'Some Article', collaborators:{ author:{ id:3, type:'users', name:'Mike Persson'}, reviewer:{ id:2, type:'groups', name:'Reviewer Group'}}},{ id:2, title:'Other Article', collaborators:{ author:{ id:2, type:'users', name:'Pete Hunt'}}},{ id:3, title:'Last Article'}], suggestions:{1:[{ id:2, title:'Other Article'},{ id:3, title:'Last Article'}]}}; Object.freeze(input);normalize(input, feedSchema).should.eql({ result:{ feed:[1,2,3], suggestions:{1:[2,3]}}, entities:{ articles:{1:{ id:1, title:'Some Article', collaborators:{ author:{ id:3, schema:'users'}, reviewer:{ id:2, schema:'groups'}}},2:{ id:2, title:'Other Article', collaborators:{ author:{ id:2, schema:'users'}}},3:{ id:3, title:'Last Article'}}, users:{2:{ id:2, type:'users', name:'Pete Hunt'},3:{ id:3, type:'users', name:'Mike Persson'}}, groups:{2:{ id:2, type:'groups', name:'Reviewer Group'}}}});
递归调用
比如某某人关注了另外的人,用户 写了一系列文章,该文章 被其他用户 订阅就是这种情况:
var article =newSchema('articles'), user =newSchema('users'), collection =newSchema('collections'), feedSchema, input; user.define({ articles:arrayOf(article)}); article.define({ collections:arrayOf(collection)}); collection.define({ subscribers:arrayOf(user)}); feedSchema ={ feed:arrayOf(article)}; input ={ feed:[{ id:1, title:'Some Article', collections:[{ id:1, title:'Awesome Writing', subscribers:[{ id:4, name:'Andy Warhol', articles:[{ id:1, title:'Some Article'}]},{ id:100, name:'T.S. Eliot', articles:[{ id:1, title:'Some Article'}]}]},{ id:7, title:'Even Awesomer', subscribers:[{ id:100, name:'T.S. Eliot', articles:[{ id:1, title:'Some Article'}]}]}]}]}; Object.freeze(input);normalize(input, feedSchema).should.eql({ result:{ feed:[1]}, entities:{ articles:{1:{ id:1, title:'Some Article', collections:[1,7]}}, collections:{1:{ id:1, title:'Awesome Writing', subscribers:[4,100]},7:{ id:7, title:'Even Awesomer', subscribers:[100]}}, users:{4:{ id:4, name:'Andy Warhol', articles:[1]},100:{ id:100, name:'T.S. Eliot', articles:[1]}}}});
上面还算好的,有些schema直接就递归声明了,比如 儿女和父母 的关系:
var user =newSchema('users'), input; user.define({ parent: user }); input ={ id:1, name:'Andy Warhol', parent:{ id:7, name:'Tom Dale', parent:{ id:4, name:'Pete Hunt'}}}; Object.freeze(input);normalize(input, user).should.eql({ result:1, entities:{ users:{1:{ id:1, name:'Andy Warhol', parent:7},7:{ id:7, name:'Tom Dale', parent:4},4:{ id:4, name:'Pete Hunt'}}}});
自动merge属性
在一个数组里面,如果id属性一致,会自动抽取并合属性成一个:
var writer =newSchema('writers'), book =newSchema('books'), schema =arrayOf(writer), input; writer.define({ books:arrayOf(book)}); input =[{ id:3, name:'Jo Rowling', isBritish:true, location:{ x:100, y:200, nested:['hello',{ world:true}]}, books:[{ id:1, soldWell:true, name:'Harry Potter'}]},{ id:3, name:'Jo Rowling', bio:'writer', location:{ x:100, y:200, nested:['hello',{ world:true}]}, books:[{ id:1, isAwesome:true, name:'Harry Potter'}]}];normalize(input, schema).should.eql({ result:[3,3], entities:{ writers:{3:{ id:3, isBritish:true, name:'Jo Rowling', bio:'writer', books:[1], location:{ x:100, y:200, nested:['hello',{ world:true}]}}}, books:{1:{ id:1, isAwesome:true, soldWell:true, name:'Harry Potter'}}}});
如果合并过程中有冲突会有提示,并自动剔除冲突的属性;比如下方同一个作者写的书,一个对象里描述“卖得好”,而在另外一个对象里却描述“卖得差”,明显是有问题的:
var writer =newSchema('writers'), book =newSchema('books'), schema =arrayOf(writer), input; writer.define({ books:arrayOf(book)}); input =[{ id:3, name:'Jo Rowling', books:[{ id:1, soldWell:true, name:'Harry Potter'}]},{ id:3, name:'Jo Rowling', books:[{ id:1, soldWell:false, name:'Harry Potter'}]}];var warnCalled =false, realConsoleWarn;functionmockWarn(){ warnCalled =true;} realConsoleWarn = console.warn; console.warn = mockWarn;normalize(input, schema).should.eql({ result:[3,3], entities:{ writers:{3:{ id:3, name:'Jo Rowling', books:[1]}}, books:{1:{ id:1, soldWell:true, name:'Harry Potter'}}}}); warnCalled.should.eq(true); console.warn = realConsoleWarn;
传入不存在的schema规范
如果应用的schma规范不存在,你还传入,就会创建一个新的父属性:
var writer =newSchema('writers'), schema = writer, input; input ={ id:'constructor', name:'Constructor', isAwesome:true};normalize(input, schema).should.eql({ result:'constructor', entities:{ writers:{ constructor:{ id:'constructor', name:'Constructor', isAwesome:true}}}
查看评论 回复
"[译]JSON数据范式化(normalizr)"的相关文章
- 上一篇:Linux硬盘挂载方法
- 下一篇:Linux系统中SVN安装、权限管理