Welcome 微信登录
编程资源 图片资源库 蚂蚁家优选 PDF转换器

首页 / 操作系统 / Linux / [译]JSON数据范式化(normalizr)

摘要 开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同数据相互引用时通过 ID 来查找。把 应用的 state 想像成数据库 。这种方法在 normalizr 文档里有详细阐述。 normalizr...开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同数据相互引用时通过 ID 来查找。把 应用的 state 想像成数据库 。这种方法在 normalizr 文档里有详细阐述。normalizr:将嵌套的JSON格式扁平化,方便被Redux利用;

目标

我们的目标是将:[{id: 1,title: "Some Article",author: {id: 1,name: "Dan"}}, {id: 2,title: "Other Article",author: {id: 1,name: "Dan"}}]
  • 数组的每个对象都糅合了三个维度 文章 、 作者
  • 按照数据范式,应当将这两个维度拆分出来,两者的联系通过id关联起来即可
我们描述上述的结构: - 返回的是一个数组(array) - 数组的对象中包含另外一个schema —— user应该比较合理的,应该是转换成:{result: [1, 2],entities: {articles: {1: {id: 1,title: "Some Article",author: 1},2: {id: 2,title: "Other Article",author: 1}},users: {1: {id: 1,name: "Dan"}}}}

如何使用

观看示例最好的,就是官方的测试文件:https://github.com/gaearon/normalizr/blob/master/test/index.js先引入 normalizrimport { normalize, Schema, arrayOf } from "normalizr";定义schemavar article = new Schema("articles"),user = new Schema("users"),collection = new Schema("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"}}}});

优势

假定请求 /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 = new Schema("articles");const user = new Schema("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 = new Schema("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 = new Schema("articles"),type = new Schema("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 = new Schema("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 = new Schema("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 。function makeSlug(article) {var posted = article.posted,title = article.title.toLowerCase().replace(" ", "-");return [title, posted.year, posted.month, posted.day].join("-");}var article = new Schema("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 = new Schema("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 = new Schema("articles"),tutorial = new Schema("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 ,相差一个sfunction guessSchema(item) {return item.type + "s";}var article = new Schema("articles"),tutorial = new Schema("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 = new Schema("articles"),tutorial = new Schema("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 = new Schema("articles"),user = new Schema("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 = new Schema("articles"),user = new Schema("users"),collection = new Schema("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 = new Schema("articles"),tutorial = new Schema("tutorials"),articleOrTutorial = { articles: article, tutorials: tutorial },user = new Schema("users"),collection = new Schema("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 = new Schema("articles"),user = new Schema("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 = new Schema("articles"),user = new Schema("users"),group = new Schema("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 = new Schema("articles"),user = new Schema("users"),collection = new Schema("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 = new Schema("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 = new Schema("writers"),book = new Schema("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 = new Schema("writers"),book = new Schema("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;function mockWarn() {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 = new Schema("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}}}本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-02/128288.htm