[译]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

先引入 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
          }
        }
      }

本文永久更新链接地址

相关内容