Normalizr-translate-chinese

官方英文文档

翻译的不太好,可以对比着看看

normalizr build status npm version npm downloads

FluxRedux 应用中根据一种模式来扁平化深层嵌套的api返回的json数据

来源(书栈小编注)

https://github.com/fa-ge/Normalizr-translate-chinese

安装

  1. npm install --save normalizr

应用例子

Flux

See flux-react-router-example.

Redux

See redux/examples/real-world.

问题

  • 你有一个返回了深层嵌套的对象的API请求
  • 你想要你的应用针对Flux 或者 Redux;
  • 你发现Store(或者Reducers)从深层嵌套的数据中获取和操作数据会非常麻烦

Normalizr采用JSON格式和一种schema,并且用嵌套实体的id来替换嵌套实体,整合所有的实体到字典对象中

For example,

  1. [{
  2. id: 1,
  3. title: 'Some Article',
  4. author: {
  5. id: 1,
  6. name: 'Dan'
  7. }
  8. }, {
  9. id: 2,
  10. title: 'Other Article',
  11. author: {
  12. id: 1,
  13. name: 'Dan'
  14. }
  15. }]

可以被范式化成(normalized to)

  1. {
  2. result: [1, 2],
  3. entities: {
  4. articles: {
  5. 1: {
  6. id: 1,
  7. title: 'Some Article',
  8. author: 1
  9. },
  10. 2: {
  11. id: 2,
  12. title: 'Other Article',
  13. author: 1
  14. }
  15. },
  16. users: {
  17. 1: {
  18. id: 1,
  19. name: 'Dan'
  20. }
  21. }
  22. }
  23. }

注意它的扁平化结构(所有的嵌套都没了)

特性

  • 实体可以嵌套在其他实体、对象、数组中
  • 可以通过结合实体的schema来表示任何类型API返回的数据
  • 相同id的实体会自动合并 (with a warning if they differ);
  • 允许使用自定义的id属性作为合并的依据 (e.g. slug).

用法

  1. import { normalize, Schema, arrayOf } from 'normalizr';

首先,为实体定义一个模式(schema)

  1. const article = new Schema('articles');
  2. const user = new Schema('users');

然后我们定义一个嵌套规则

  1. article.define({
  2. author: user,
  3. contributors: arrayOf(user)
  4. });

现在我们可以在处理API请求返回数据的方法中使用这个schema:

  1. const ServerActionCreators = {
  2. // 有两个带有不同响应对象的schemas的XHR endpoints----------------------------
  3. // 我们可以用前面定义的schema对象来表示他们:
  4. receiveOneArticle(response) {
  5. // 在这里,这个返回的数据是一个包含了一篇文章的对象
  6. // 把文章的schema作为normalize方法的第二个参数,让他能正确的遍历响应对象并且把所有的实体都整合到一起
  7. // BEFORE:
  8. // {
  9. // id: 1,
  10. // title: 'Some Article',
  11. // author: {
  12. // id: 7,
  13. // name: 'Dan'
  14. // },
  15. // contributors: [{
  16. // id: 10,
  17. // name: 'Abe'
  18. // }, {
  19. // id: 15,
  20. // name: 'Fred'
  21. // }]
  22. // }
  23. //
  24. // AFTER:
  25. // {
  26. // result: 1, // <--- 注意对象是由ID来引用的
  27. // entities: {
  28. // articles: {
  29. // 1: {
  30. // author: 7, // <--- 同样适用于在其它实体中的引用
  31. // contributors: [10, 15]
  32. // ...}
  33. // },
  34. // users: {
  35. // 7: { ... },
  36. // 10: { ... },
  37. // 15: { ... }
  38. // }
  39. // }
  40. // }
  41. response = normalize(response, article);
  42. AppDispatcher.handleServerAction({
  43. type: ActionTypes.RECEIVE_ONE_ARTICLE,
  44. response
  45. });
  46. },
  47. receiveAllArticles(response) {
  48. // 这里,返回的数据是一个对象,他的key为'articles'并且该key指向了一个包含文章对象的数组
  49. // 把 { articles: arrayOf(article) }作为normalize方法的第二个参数,让他能正确的遍历响应对象并且把所有的实体都整合到一起
  50. // BEFORE:
  51. // {
  52. // articles: [{
  53. // id: 1,
  54. // title: 'Some Article',
  55. // author: {
  56. // id: 7,
  57. // name: 'Dan'
  58. // },
  59. // ...
  60. // },
  61. // ...
  62. // ]
  63. // }
  64. //
  65. // AFTER:
  66. // {
  67. // result: {
  68. // articles: [1, 2, ...] // <--- 注意对象数组转换成ID数组的方式
  69. // },
  70. // entities: {
  71. // articles: {
  72. // 1: { author: 7, ... }, // <--- 同样适用于在其它实体中的引用
  73. // 2: { ... },
  74. // ...
  75. // },
  76. // users: {
  77. // 7: { ... },
  78. // ..
  79. // }
  80. // }
  81. // }
  82. response = normalize(response, {
  83. articles: arrayOf(article)
  84. });
  85. AppDispatcher.handleServerAction({
  86. type: ActionTypes.RECEIVE_ALL_ARTICLES,
  87. response
  88. });
  89. }
  90. }

Finally, different Stores can tune in to listen to all API responses and grab entity lists from action.response.entities:

  1. AppDispatcher.register((payload) => {
  2. const { action } = payload;
  3. if (action.response && action.response.entities && action.response.entities.users) {
  4. mergeUsers(action.response.entities.users);
  5. UserStore.emitChange();
  6. break;
  7. }
  8. });

API Reference

new Schema(key, [options])

Schema 允许你定义一个通过API返回的实体的类型
这应该与你服务器上代码的模型相对应

key参数允许你为这类实体指定一个名字(译者的理解其实就是数据库表名)

  1. const article = new Schema('articles');
  2. // 你可以使用自定义的id属性
  3. const article = new Schema('articles', { idAttribute: 'slug' });
  4. // 或者指定一个函数来生成
  5. function generateSlug(entity) { /* ... */ }
  6. const article = new Schema('articles', { idAttribute: generateSlug });
  7. // You can also specify meta properties to be used for customizing the output in assignEntity (见下)
  8. const article = new Schema('articles', { idAttribute: 'slug', meta: { removeProps: ['publisher'] }});
  9. // You can specify custom `assignEntity` function to be run after the `assignEntity` function passed to `normalize`
  10. const article = new Schema('articles', { assignEntity: function (output, key, value, input) {
  11. if (key === 'id_str') {
  12. output.id = value;
  13. if ('id_str' in output) {
  14. delete output.id_str;
  15. }
  16. } else {
  17. output[key] = value;
  18. }
  19. }})
  20. // 你可以为实体指定默认的值
  21. const article = new Schema('articles', { defaults: { likes: 0 } });

Schema.prototype.define(nestedSchema)

允许你指定不同实体之间的关系

  1. const article = new Schema('articles');
  2. const user = new Schema('users');
  3. article.define({
  4. author: user
  5. });

Schema.prototype.getKey()

返回这个schema的key

  1. const article = new Schema('articles');
  2. article.getKey();
  3. // articles

Schema.prototype.getIdAttribute()

返回这个schema的id属性

  1. const article = new Schema('articles');
  2. const slugArticle = new Schema('articles', { idAttribute: 'slug' });
  3. article.getIdAttribute();
  4. // id
  5. slugArticle.getIdAttribute();
  6. // slug

Schema.prototype.getDefaults()

返回这个schema的默认值

  1. const article = new Schema('articles', { defaults: { likes: 0 } });
  2. article.getDefaults();
  3. // { likes: 0 }

arrayOf(schema, [options])

描述了一个schema的数组作为参数被传递

  1. const article = new Schema('articles');
  2. const user = new Schema('users');
  3. article.define({
  4. author: user,
  5. contributors: arrayOf(user)
  6. });

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

  1. const article = new Schema('articles');
  2. const image = new Schema('images');
  3. const video = new Schema('videos');
  4. const asset = {
  5. images: image,
  6. videos: video
  7. };
  8. // You can specify the name of the attribute that determines the schema
  9. article.define({
  10. assets: arrayOf(asset, { schemaAttribute: 'type' })
  11. });
  12. // Or you can specify a function to infer it
  13. function inferSchema(entity) { /* ... */ }
  14. article.define({
  15. assets: arrayOf(asset, { schemaAttribute: inferSchema })
  16. });

valuesOf(schema, [options])

Describes a map whose values follow the schema passed as argument.

  1. const article = new Schema('articles');
  2. const user = new Schema('users');
  3. article.define({
  4. collaboratorsByRole: valuesOf(user)
  5. });

If the map contains entities with different schemas, you can use the schemaAttribute option to specify which schema to use for each entity:

  1. const article = new Schema('articles');
  2. const user = new Schema('users');
  3. const group = new Schema('groups');
  4. const collaborator = {
  5. users: user,
  6. groups: group
  7. };
  8. // You can specify the name of the attribute that determines the schema
  9. article.define({
  10. collaboratorsByRole: valuesOf(collaborator, { schemaAttribute: 'type' })
  11. });
  12. // Or you can specify a function to infer it
  13. function inferSchema(entity) { /* ... */ }
  14. article.define({
  15. collaboratorsByRole: valuesOf(collaborator, { schemaAttribute: inferSchema })
  16. });

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.

  1. const group = new Schema('groups');
  2. const user = new Schema('users');
  3. // a member can be either a user or a group
  4. const member = {
  5. users: user,
  6. groups: group
  7. };
  8. // You can specify the name of the attribute that determines the schema
  9. group.define({
  10. owner: unionOf(member, { schemaAttribute: 'type' })
  11. });
  12. // Or you can specify a function to infer it
  13. function inferSchema(entity) { /* ... */ }
  14. group.define({
  15. creator: unionOf(member, { schemaAttribute: inferSchema })
  16. });

A unionOf schema can also be combined with arrayOf and valuesOf with the same behavior as each supplied with the schemaAttribute option.

  1. const group = new Schema('groups');
  2. const user = new Schema('users');
  3. const member = unionOf({
  4. users: user,
  5. groups: group
  6. }, { schemaAttribute: 'type' });
  7. group.define({
  8. owner: member,
  9. members: arrayOf(member),
  10. relationships: valuesOf(member)
  11. });

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.

  1. const article = new Schema('articles');
  2. const user = new Schema('users');
  3. article.define({
  4. author: user,
  5. contributors: arrayOf(user),
  6. meta: {
  7. likes: arrayOf({
  8. user: user
  9. })
  10. }
  11. });
  12. // ...
  13. // Normalize one article object
  14. const json = { id: 1, author: ... };
  15. const normalized = normalize(json, article);
  16. // Normalize an array of article objects
  17. const arr = [{ id: 1, author: ... }, ...]
  18. const normalized = normalize(arr, arrayOf(article));
  19. // Normalize an array of article objects, referenced by an object key:
  20. const wrappedArr = { articles: [{ id: 1, author: ... }, ...] }
  21. const normalized = normalize(wrappedArr, {
  22. articles: arrayOf(article)
  23. });

通过例子来解释

假如你有返回以下schema的API请求/articles:

  1. articles: article*
  2. article: {
  3. author: user,
  4. likers: user*
  5. primary_collection: collection?
  6. collections: collection*
  7. }
  8. collection: {
  9. curator: user
  10. }

如果不做范式化,你的store需要清楚的知道返回数据的结构
比如, UserStore在获取到请求结果的时候会包含很多样板代码来获取新用户

  1. // 如果不做范式化, 你需要对每一个store都做这些
  2. AppDispatcher.register((payload) => {
  3. const { action } = payload;
  4. switch (action.type) {
  5. case ActionTypes.RECEIVE_USERS:
  6. mergeUsers(action.rawUsers);
  7. break;
  8. case ActionTypes.RECEIVE_ARTICLES:
  9. action.rawArticles.forEach(rawArticle => {
  10. mergeUsers([rawArticle.user]);
  11. mergeUsers(rawArticle.likers);
  12. mergeUsers([rawArticle.primaryCollection.curator]);
  13. rawArticle.collections.forEach(rawCollection => {
  14. mergeUsers(rawCollection.curator);
  15. });
  16. });
  17. UserStore.emitChange();
  18. break;
  19. }
  20. });

Normalizr解决了这个问题,通过把嵌套的实体用id替代把请求返回的数据变成了一个扁平化的结构的对象

  1. {
  2. result: [12, 10, 3, ...],
  3. entities: {
  4. articles: {
  5. 12: {
  6. authorId: 3,
  7. likers: [2, 1, 4],
  8. primaryCollection: 12,
  9. collections: [12, 11]
  10. },
  11. ...
  12. },
  13. users: {
  14. 3: {
  15. name: 'Dan'
  16. },
  17. 2: ...,
  18. 4: ....
  19. },
  20. collections: {
  21. 12: {
  22. curator: 2,
  23. name: 'Stuff'
  24. },
  25. ...
  26. }
  27. }
  28. }

UserStore的代码可以像这样被重写

  1. // 做了范式化后,所有的用户都在action.response.entities.users中
  2. AppDispatcher.register((payload) => {
  3. const { action } = payload;
  4. if (action.response && action.response.entities && action.response.entities.users) {
  5. mergeUsers(action.response.entities.users);
  6. UserStore.emitChange();
  7. break;
  8. }
  9. });

依赖

  • 一些方法依赖 lodash, 比如 isObject, isEqualmapValues

Browser Support

Modern browsers with ES5 environments are supported.
The minimal supported IE version is IE 9.

Running Tests

  1. git clone https://github.com/gaearon/normalizr.git
  2. cd normalizr
  3. npm install
  4. npm test # run tests once
  5. npm 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.