Normalizr-translate-chinese
翻译的不太好,可以对比着看看
normalizr

在Flux和Redux 应用中根据一种模式来扁平化深层嵌套的api返回的json数据
安装
npm install --save normalizr
应用例子
Flux
See flux-react-router-example.
Redux
See redux/examples/real-world.
问题
Normalizr采用JSON格式和一种schema,并且用嵌套实体的id来替换嵌套实体,整合所有的实体到字典对象中
For example,
[{id: 1,title: 'Some Article',author: {id: 1,name: 'Dan'}}, {id: 2,title: 'Other Article',author: {id: 1,name: 'Dan'}}]
可以被范式化成(normalized to)
{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'}}}}
注意它的扁平化结构(所有的嵌套都没了)
特性
- 实体可以嵌套在其他实体、对象、数组中
- 可以通过结合实体的schema来表示任何类型API返回的数据
- 相同id的实体会自动合并 (with a warning if they differ);
- 允许使用自定义的id属性作为合并的依据 (e.g. slug).
用法
import { normalize, Schema, arrayOf } from 'normalizr';
首先,为实体定义一个模式(schema)
const article = new Schema('articles');const user = new Schema('users');
然后我们定义一个嵌套规则
article.define({author: user,contributors: arrayOf(user)});
现在我们可以在处理API请求返回数据的方法中使用这个schema:
const ServerActionCreators = {// 有两个带有不同响应对象的schemas的XHR endpoints----------------------------// 我们可以用前面定义的schema对象来表示他们:receiveOneArticle(response) {// 在这里,这个返回的数据是一个包含了一篇文章的对象// 把文章的schema作为normalize方法的第二个参数,让他能正确的遍历响应对象并且把所有的实体都整合到一起// BEFORE:// {// id: 1,// title: 'Some Article',// author: {// id: 7,// name: 'Dan'// },// contributors: [{// id: 10,// name: 'Abe'// }, {// id: 15,// name: 'Fred'// }]// }//// AFTER:// {// result: 1, // <--- 注意对象是由ID来引用的// entities: {// articles: {// 1: {// author: 7, // <--- 同样适用于在其它实体中的引用// contributors: [10, 15]// ...}// },// users: {// 7: { ... },// 10: { ... },// 15: { ... }// }// }// }response = normalize(response, article);AppDispatcher.handleServerAction({type: ActionTypes.RECEIVE_ONE_ARTICLE,response});},receiveAllArticles(response) {// 这里,返回的数据是一个对象,他的key为'articles'并且该key指向了一个包含文章对象的数组// 把 { articles: arrayOf(article) }作为normalize方法的第二个参数,让他能正确的遍历响应对象并且把所有的实体都整合到一起// BEFORE:// {// articles: [{// id: 1,// title: 'Some Article',// author: {// id: 7,// name: 'Dan'// },// ...// },// ...// ]// }//// AFTER:// {// result: {// articles: [1, 2, ...] // <--- 注意对象数组转换成ID数组的方式// },// entities: {// articles: {// 1: { author: 7, ... }, // <--- 同样适用于在其它实体中的引用// 2: { ... },// ...// },// users: {// 7: { ... },// ..// }// }// }response = normalize(response, {articles: arrayOf(article)});AppDispatcher.handleServerAction({type: ActionTypes.RECEIVE_ALL_ARTICLES,response});}}
Finally, different Stores can tune in to listen to all API responses and grab entity lists from action.response.entities:
AppDispatcher.register((payload) => {const { action } = payload;if (action.response && action.response.entities && action.response.entities.users) {mergeUsers(action.response.entities.users);UserStore.emitChange();break;}});
API Reference
new Schema(key, [options])
Schema 允许你定义一个通过API返回的实体的类型 这应该与你服务器上代码的模型相对应
key参数允许你为这类实体指定一个名字(译者的理解其实就是数据库表名)
const article = new Schema('articles');// 你可以使用自定义的id属性const article = new Schema('articles', { idAttribute: 'slug' });// 或者指定一个函数来生成function generateSlug(entity) { /* ... */ }const article = new Schema('articles', { idAttribute: generateSlug });// You can also specify meta properties to be used for customizing the output in assignEntity (见下)const article = new Schema('articles', { idAttribute: 'slug', meta: { removeProps: ['publisher'] }});// You can specify custom `assignEntity` function to be run after the `assignEntity` function passed to `normalize`const article = new Schema('articles', { assignEntity: function (output, key, value, input) {if (key === 'id_str') {output.id = value;if ('id_str' in output) {delete output.id_str;}} else {output[key] = value;}}})// 你可以为实体指定默认的值const article = new Schema('articles', { defaults: { likes: 0 } });
Schema.prototype.define(nestedSchema)
允许你指定不同实体之间的关系
const article = new Schema('articles');const user = new Schema('users');article.define({author: user});
Schema.prototype.getKey()
返回这个schema的key
const article = new Schema('articles');article.getKey();// articles
Schema.prototype.getIdAttribute()
返回这个schema的id属性
const article = new Schema('articles');const slugArticle = new Schema('articles', { idAttribute: 'slug' });article.getIdAttribute();// idslugArticle.getIdAttribute();// slug
Schema.prototype.getDefaults()
返回这个schema的默认值
const article = new Schema('articles', { defaults: { likes: 0 } });article.getDefaults();// { likes: 0 }
arrayOf(schema, [options])
描述了一个schema的数组作为参数被传递
const article = new Schema('articles');const user = new Schema('users');article.define({author: user,contributors: arrayOf(user)});
If the array contains entities with different schemas, you can use the schemaAttribute option to specify which schema to use for each entity:
如果一个数组包含了具有不同schema的实体,你可以使用schemaAttribute选项来为每一个实体指定schema
const article = new Schema('articles');const image = new Schema('images');const video = new Schema('videos');const asset = {images: image,videos: video};// You can specify the name of the attribute that determines the schemaarticle.define({assets: arrayOf(asset, { schemaAttribute: 'type' })});// Or you can specify a function to infer itfunction inferSchema(entity) { /* ... */ }article.define({assets: arrayOf(asset, { schemaAttribute: inferSchema })});
valuesOf(schema, [options])
Describes a map whose values follow the schema passed as argument.
const article = new Schema('articles');const user = new Schema('users');article.define({collaboratorsByRole: valuesOf(user)});
If the map contains entities with different schemas, you can use the schemaAttribute option to specify which schema to use for each entity:
const article = new Schema('articles');const user = new Schema('users');const group = new Schema('groups');const collaborator = {users: user,groups: group};// You can specify the name of the attribute that determines the schemaarticle.define({collaboratorsByRole: valuesOf(collaborator, { schemaAttribute: 'type' })});// Or you can specify a function to infer itfunction inferSchema(entity) { /* ... */ }article.define({collaboratorsByRole: valuesOf(collaborator, { schemaAttribute: inferSchema })});
unionOf(schemaMap, [options])
Describe a schema which is a union of multiple schemas. This is useful if you need the polymorphic behavior provided by arrayOf or valuesOf but for non-collection fields.
Use the required schemaAttribute option to specify which schema to use for each entity.
const group = new Schema('groups');const user = new Schema('users');// a member can be either a user or a groupconst member = {users: user,groups: group};// You can specify the name of the attribute that determines the schemagroup.define({owner: unionOf(member, { schemaAttribute: 'type' })});// Or you can specify a function to infer itfunction inferSchema(entity) { /* ... */ }group.define({creator: unionOf(member, { schemaAttribute: inferSchema })});
A unionOf schema can also be combined with arrayOf and valuesOf with the same behavior as each supplied with the schemaAttribute option.
const group = new Schema('groups');const user = new Schema('users');const member = unionOf({users: user,groups: group}, { schemaAttribute: 'type' });group.define({owner: member,members: arrayOf(member),relationships: valuesOf(member)});
normalize(obj, schema, [options])
Normalizes object according to schema.
Passed schema should be a nested object reflecting the structure of API response.
You may optionally specify any of the following options:
assignEntity(function): This is useful if your backend emits additional fields, such as separate ID fields, you’d like to delete in the normalized entity. See the tests and the discussion for a usage example.mergeIntoEntity(function): You can use this to resolve conflicts when merging entities with the same key. See the test and the discussion for a usage example.
const article = new Schema('articles');const user = new Schema('users');article.define({author: user,contributors: arrayOf(user),meta: {likes: arrayOf({user: user})}});// ...// Normalize one article objectconst json = { id: 1, author: ... };const normalized = normalize(json, article);// Normalize an array of article objectsconst arr = [{ id: 1, author: ... }, ...]const normalized = normalize(arr, arrayOf(article));// Normalize an array of article objects, referenced by an object key:const wrappedArr = { articles: [{ id: 1, author: ... }, ...] }const normalized = normalize(wrappedArr, {articles: arrayOf(article)});
通过例子来解释
假如你有返回以下schema的API请求/articles:
articles: article*article: {author: user,likers: user*primary_collection: collection?collections: collection*}collection: {curator: user}
如果不做范式化,你的store需要清楚的知道返回数据的结构
比如, UserStore在获取到请求结果的时候会包含很多样板代码来获取新用户
// 如果不做范式化, 你需要对每一个store都做这些AppDispatcher.register((payload) => {const { action } = payload;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;}});
Normalizr解决了这个问题,通过把嵌套的实体用id替代把请求返回的数据变成了一个扁平化的结构的对象
{result: [12, 10, 3, ...],entities: {articles: {12: {authorId: 3,likers: [2, 1, 4],primaryCollection: 12,collections: [12, 11]},...},users: {3: {name: 'Dan'},2: ...,4: ....},collections: {12: {curator: 2,name: 'Stuff'},...}}}
UserStore的代码可以像这样被重写
// 做了范式化后,所有的用户都在action.response.entities.users中AppDispatcher.register((payload) => {const { action } = payload;if (action.response && action.response.entities && action.response.entities.users) {mergeUsers(action.response.entities.users);UserStore.emitChange();break;}});
依赖
- 一些方法依赖
lodash, 比如isObject,isEqual和mapValues
Browser Support
Modern browsers with ES5 environments are supported.
The minimal supported IE version is IE 9.
Running Tests
git clone https://github.com/gaearon/normalizr.gitcd normalizrnpm installnpm test # run tests oncenpm run test:watch # run test watcher
Credits
Normalizr was originally created by Dan Abramov and inspired by a conversation with Jing Chen.
It has since received contributions from different community members.
