[译]JSON数据范式化(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
先引入 normalizr
:
import { normalize, Schema, arrayOf } from 'normalizr';
定义 schema
:
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' } } } });
优势
假定请求 /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
,相差一个 s
:
function 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 } } }
</div>