normalizr API 完全指南:normalize / denormalize 与五大 Schema 的源码级解析
2026/9/20 23:44:07 网站建设 项目流程

normalizr API 完全指南:normalize / denormalize 与五大 Schema 的源码级解析

【免费下载链接】normalizrNormalizes nested JSON according to a schema项目地址: https://gitcode.com/gh_mirrors/no/normalizr

本篇技术指南以 normalizr 的官方 API 文档(docs/api.md)为骨架,系统讲解normalizedenormalize两个核心函数与ArrayEntityObjectUnionValues五种 Schema 的全部参数、选项、实例方法与输出格式,并结合本仓库源码(src/index.js 与 src/schemas/)揭示其底层实现原理。读完本文,你将能针对任意嵌套 JSON 设计 Schema、精准预测归一化输出,并掌握多态归一化、循环引用、ID 生成策略等进阶用法,直接应用于 Redux/React 状态管理场景。

一、normalize:按 Schema 将嵌套 JSON 拍平为实体字典

normalize(data, schema)是 normalizr 的入口函数,负责把深度嵌套的 JSON(或普通 JS 对象)按照给定的 schema 定义转换为扁平化的"实体字典 + 引用树"结构。

1.1 函数签名与参数

  • data必填。需要归一化的输入 JSON 或普通 JS 对象。
  • schema必填。一个 schema 定义(EntityArrayObjectUnionValues或它们的嵌套组合,也支持字面量简写语法)。

1.2 基本用法

import { normalize, schema } from 'normalizr'; const myData = { users: [{ id: 1 }, { id: 2 }] }; const user = new schema.Entity('users'); const mySchema = { users: [user] }; const normalizedData = normalize(myData, mySchema);

1.3 输出结构

{ result: { users: [ 1, 2 ] }, entities: { users: { '1': { id: 1 }, '2': { id: 2 } } } }

输出的两个关键部分:

  • result:归一化后的"数据骨架",所有被识别为实体的对象都被替换为其 ID(对集合类 schema 则是 ID 数组 / ID 映射);
  • entities:按实体类型的key分组的字典,entities.users['1']可以直接通过 ID 查询。

从源码看,normalize的实现非常简洁(src/index.js):它先校验输入必须是对象,否则抛出Unexpected input given to normalize...错误;随后初始化空的entities字典与visitedEntities(用于防止循环引用导致死循环),再通过递归的visit函数遍历整棵树。visit的核心逻辑(src/index.js)是:

  • 如果当前值不是对象(typeof value !== 'object'),直接原样返回;
  • 如果 schema 本身没有normalize方法(即传入的是字面量对象{ ... }或数组[ ... ]),则按数组/对象分别委托给ArrayUtils.normalizeObjectUtils.normalize(这正是简写语法能工作的底层原因);
  • 否则调用 schema 实例的normalize方法继续递归。

addEntities(src/index.js)则负责把处理完的实体写入entities字典,若同一key下已存在相同 ID 的实体,会调用schema.merge(即mergeStrategy)合并。

二、denormalize:归一化数据的逆操作

denormalize(input, schema, entities)根据 schema 与实体字典,把归一化结果还原为嵌套对象,是normalize的逆过程。

2.1 函数签名与参数

  • input必填。需要反归一化的输入,通常是normalize输出中result键的值。
  • schema必填。与当初用于生成input的 schema 定义一致。
  • entities必填。以实体 schema 名(key)为键的对象,键值为各实体 ID 到实体数据的映射;也支持 Immutable 数据结构。

2.2 基本用法

import { denormalize, schema } from 'normalizr'; const user = new schema.Entity('users'); const mySchema = { users: [user] }; const entities = { users: { '1': { id: 1 }, '2': { id: 2 } } }; const denormalizedData = denormalize({ users: [1, 2] }, mySchema, entities);

2.3 输出

{ users: [{ id: 1 }, { id: 2 }]; }

2.4 重要注意事项

性能警告:不要过早地对数据进行反归一化。把数据提前还原成庞大、深嵌套的对象,在 React(以及其他框架)应用中可能造成明显的性能问题——这正是推荐"存扁平、取时还原"模式的原因。

循环引用:如果 schema 和数据存在递归引用(例如实体 A 引用实体 B、B 又引用 A),反归一化时只有第一个被遇到的实体实例会被完整展开,后续引用只会返回对应的id。这一点在源码中有明确体现(src/index.js):unvisitEntity使用cache缓存每个实体,先cache[schema.key][id] = entityCopy占位再递归denormalize,从而保证"如果递归中再次引用到它,引用已存在"。

2.5 底层实现:unvisit 与缓存机制

denormalize内部通过getUnvisit(entities)构建递归闭包(src/index.js):

  • 若 schema 是字面量对象/数组,同样委托给ObjectUtils.denormalize/ArrayUtils.denormalize
  • null/undefined直接返回原值;
  • 若命中EntitySchema且实体缺失,会调用schema.fallback(id, schema)(对应fallbackStrategy选项);
  • 对集合类 schema 则调用其自身的denormalize

getEntities(src/index.js)支持两种数据源:普通 JS 对象(entities[schemaKey][entityOrId])与 Immutable 数据(通过entities.getIn([schemaKey, entityOrId.toString()])查询)。Immutable 兼容性的判断与实现位于 src/schemas/ImmutableUtils.js:通过检测对象上是否存在__ownerIDImmutable.Map)或_map.__ownerIDImmutable.Record)来识别 Immutable 实例,且反归一化时会把键统一转成字符串再set回原对象,规避 Immutable Map 写入时键被强转字符串的问题。

三、schema 总览:五种 Schema 与简写语法

schema命名空间下共导出五个 Schema 类(见 src/index.js):

Schema用途简写语法
schema.Array归一化一个由同构实体构成的数组[ mySchema ]
schema.Entity定义一个实体类型
schema.Object定义普通对象映射,其值需要归一化{ ... }
schema.Union描述多种 Schema 的联合(多态,非集合场景)
schema.Values描述值遵循给定 Schema 的 Map

ArrayUnionValues三者共享多态基类PolymorphicSchema(src/schemas/Polymorphic.js),其核心是schemaAttribute(schema 判别属性)机制与inferSchema推断逻辑,下文分别展开。所有 Schema 类都提供define(definition)实例方法,用于与原始定义合并,这在构建循环引用的 Schema 时必不可少。

四、schema.Array(definition, schemaAttribute):数组归一化

Array(definition, schemaAttribute)创建用于归一化数组的 Schema。若输入值不是数组而是Object,归一化结果会是该对象各 value 组成的数组(源码中getValues的实现见 src/schemas/Array.js)。

提示:同样的行为可以直接用简写语法[ mySchema ]表达。

4.1 参数

  • definition必填。可以是单一 Schema(表示数组中元素都是该 Schema),也可以是"schema 名 → Schema"的映射(表示数组中元素类型不唯一)。
  • schemaAttribute可选(当definition为映射时必填)。每个实体上用于决定采用映射中哪个 Schema 的属性。可以是字符串或函数;若为函数,依次接收:
    • value:当前实体的输入值;
    • parent:输入数组的父对象;
    • key:输入数组在父对象上出现的键。

4.2 实例方法

  • define(definition):将传入的 definition 与构造时的原始 definition 合并,常用于构建循环引用的 Schema。

4.3 用法一:单一实体类型的数组

const data = [{ id: '123', name: 'Jim' }, { id: '456', name: 'Jane' }]; const userSchema = new schema.Entity('users'); const userListSchema = new schema.Array(userSchema); // or use shorthand syntax: const userListSchema = [userSchema]; const normalizedData = normalize(data, userListSchema);

输出:

{ entities: { users: { '123': { id: '123', name: 'Jim' }, '456': { id: '456', name: 'Jane' } } }, result: [ '123', '456' ] }

4.4 用法二:多类型实体数组(多态数组)

当输入数组包含多种实体类型时,必须定义 schema 映射:

注意:如果数据中出现了你没有提供映射的对象,该原始对象会原样出现在 result 中,且不会为其创建实体

const data = [{ id: 1, type: 'admin' }, { id: 2, type: 'user' }]; const userSchema = new schema.Entity('users'); const adminSchema = new schema.Entity('admins'); const myArray = new schema.Array( { admins: adminSchema, users: userSchema }, (input, parent, key) => `${input.type}s` ); const normalizedData = normalize(data, myArray);

输出:

{ entities: { admins: { '1': { id: 1, type: 'admin' } }, users: { '2': { id: 2, type: 'user' } } }, result: [ { id: 1, schema: 'admins' }, { id: 2, schema: 'users' } ] }

注意多态输出的关键差异:result中每个元素不再是单纯的 ID,而是{ id, schema }对象,schema字段记录了该元素实际命中的映射键,供denormalize时还原用。ArraySchema.normalize(src/schemas/Array.js)还会过滤掉undefined/null的归一化结果;其denormalize则逐元素调用denormalizeValue(src/schemas/Polymorphic.js)——单 Schema 模式下直接以 ID 查询,多态模式下则读取value.schema定位映射 Schema 后再还原。

五、schema.Entity(key, definition = {}, options = {}):核心实体 Schema

Entity是 normalizr 中最核心、参数最丰富的 Schema,用于定义"实体类型"及其中嵌套的实体关系。

5.1 参数

  • key必填。该类型实体在归一化结果中统一存放的键名,必须是字符串。源码中会校验key必须是字符串,否则抛出Expected a string key for Entity, but found ${key}.(src/schemas/Entity.js)。
  • definition:该实体内部嵌套实体的定义,默认为空对象。你只需要声明那些存放嵌套实体的键,其余值会原样复制到归一化后的实体输出中。
  • options:实体行为选项,详见下表。

5.2 options 详解

选项类型默认值说明
idAttributestring|function'id'指定实体唯一 ID 所在属性。传函数时返回 ID 的。该函数可能被执行多次,因此生成结果必须每次一致——使用uuid之类的随机生成器会导致难以预料的错误
mergeStrategy(entityA, entityB) => entity将较新发现的实体合并到先前实体上({ ...entityA, ...entityB }遇到两个相同 ID 实体时的合并策略
processStrategy(value, parent, key) => entity返回输入实体的浅拷贝归一化前对实体的预处理,可用来附加额外数据、补默认值、或彻底改造实体。建议始终返回输入的拷贝,不要修改原始对象
fallbackStrategy(key, schema) => entity返回undefined反归一化时遇到 ID 引用缺失实体的兜底策略,可返回一个占位实体

各函数的参数说明:

  • processStrategy(value, parent, key)value为实体输入值,parent为输入数组的父对象,key为输入数组在父对象上出现的键;
  • mergeStrategy(entityA, entityB)entityA为先前已存储的实体,entityB为新发现的实体;
  • fallbackStrategy(key, schema)key为缺失实体的 ID,schema为缺失实体的 Schema。

5.3 实例方法与实例属性

实例方法:

  • define(definition):合并 definition 到原始定义,用于循环引用场景。

实例属性(getter):

  • key:返回构造时传入的key
  • idAttribute:返回构造时在 options 中传入的idAttribute

从源码看(src/schemas/Entity.js),构造函数会解构四个选项并做默认值处理:idAttribute默认为字符串'id'(通过getDefaultGetId包装成按属性取值,同时兼容 Immutable 的.get);mergeStrategy默认{ ...entityA, ...entityB }processStrategy默认(input) => ({ ...input })fallbackStrategy默认返回undefined

5.4 综合用法示例

const data = { id_str: '123', url: 'https://twitter.com', user: { id_str: '456', name: 'Jimmy' } }; const user = new schema.Entity('users', {}, { idAttribute: 'id_str' }); const tweet = new schema.Entity( 'tweets', { user: user }, { idAttribute: 'id_str', // Apply everything from entityB over entityA, except for "favorites" mergeStrategy: (entityA, entityB) => ({ ...entityA, ...entityB, favorites: entityA.favorites }), // Remove the URL field from the entity processStrategy: (entity) => omit(entity, 'url') } ); const normalizedData = normalize(data, tweet);

输出:

{ entities: { tweets: { '123': { id_str: '123', user: '456' } }, users: { '456': { id_str: '456', name: 'Jimmy' } } }, result: '123' }

可以看到:tweets实体中的嵌套user被替换为 ID'456'users实体单独成表;processStrategy剥离了url字段;result则是 tweet 自身的 ID。

5.5 idAttribute 的函数用法

idAttribute传入函数时,函数必须返回 ID 的(不是键名)。例如当同一用户有id和可选的guest_id时,可以组合两者生成复合 ID:

const data = [{ id: '1', guest_id: null, name: 'Esther' }, { id: '1', guest_id: '22', name: 'Tom' }]; const patronsSchema = new schema.Entity('patrons', undefined, { // idAttribute *functions* must return the ids **value** (not key) idAttribute: (value) => (value.guest_id ? `${value.id}-${value.guest_id}` : value.id) }); normalize(data, [patronsSchema]);

输出:

{ entities: { patrons: { '1': { id: '1', guest_id: null, name: 'Esther' }, '1-22': { id: '1', guest_id: '22', name: 'Tom' }, } }, result: ['1', '1-22'] }

注意:由于两个输入对象都有id: '1',若不自定义idAttribute,后一个会通过mergeStrategy合并覆盖前一个;这里用函数生成了不同 ID,两条数据得以共存。

5.6 fallbackStrategy 用法:缺失实体兜底

denormalize遇到引用但实体字典中不存在的 ID 时,默认返回undefinedfallbackStrategy允许你生成一个占位实体,避免下游空指针:

const users = { '1': { id: '1', name: "Emily", requestState: 'SUCCEEDED' }, '2': { id: '2', name: "Douglas", requestState: 'SUCCEEDED' } }; const books = { '1': {id: '1', name: "Book 1", author: 1 }, '2': {id: '2', name: "Book 2", author: 2 }, '3': {id: '3', name: "Book 3", author: 3 } }; const authorSchema = new schema.Entity('authors', {}, { fallbackStrategy: (key, schema) => { return { [schema.idAttribute]: key, name: 'Unknown', requestState: 'NONE' }; } }); const bookSchema = new schema.Entity('books', { author: authorSchema }); denormalize([1, 2, 3], [bookSchema], { books, authors: users })

输出:

[ { id: '1', name: "Book 1", author: { id: '1', name: "Emily", requestState: 'SUCCEEDED' } }, { id: '2', name: "Book 2", author: { id: '2', name: "Douglas", requestState: 'SUCCEEDED' }, }, { id: '3', name: "Book 3", author: { id: '3', name: "Unknown", requestState: 'NONE' }, } ]

这里第 3 本书引用了author: 3,但authors字典中没有 ID 为3的用户,fallbackStrategy便生成了一个{ id: '3', name: 'Unknown', requestState: 'NONE' }占位实体。该调用链对应源码 src/index.js:unvisitEntitygetEntity(id, schema)返回undefined且命中EntitySchema时,会执行schema.fallback(id, schema)

5.7 归一化内部流程:visitedEntities 防循环

EntitySchema.normalize(src/schemas/Entity.js)的流程是:先用idAttribute取 ID → 借助visitedEntities[entityType][id]记录已访问的输入对象引用(若当前输入对象已被访问过则直接返回 ID,防止循环引用无限递归)→ 调用processStrategy预处理 → 遍历this.schema中声明了嵌套 Schema 的键,对每个对象值递归visit→ 最后addEntity写入实体字典并返回 ID。若 schema 中的值是函数(支持"惰性 schema"),会先以input调用解析出真实 Schema(src/schemas/Entity.js)。

六、schema.Object(definition):普通对象映射

Object(definition)定义普通对象映射,其值需要归一化为实体。

提示:同样的行为可以用简写语法{ ... }直接表达。

6.1 参数

  • definition必填。对象内嵌套实体的定义,默认为空对象。只需要声明存放实体的键,其余值会原样复制到归一化输出。

6.2 实例方法

  • define(definition):合并 definition,用于循环引用场景。

6.3 用法

// Example data response const data = { users: [{ id: '123', name: 'Beth' }] }; const user = new schema.Entity('users'); const responseSchema = new schema.Object({ users: new schema.Array(user) }); // or shorthand const responseSchema = { users: new schema.Array(user) }; const normalizedData = normalize(data, responseSchema);

输出:

{ entities: { users: { '123': { id: '123', name: 'Beth' } } }, result: { users: [ '123' ] } }

从源码看(src/schemas/Object.js),Object的归一化会对 schema 中的每个键调用visit(input[key], input, key, ...),且归一化结果为null/undefined的键会直接从输出对象中删除delete object[key]),这保证result里不会残留空值引用;denormalize时则只对值非空(!= null)的键进行还原(src/schemas/Object.js)。

七、schema.Union(definition, schemaAttribute):非集合多态

Union(definition, schemaAttribute)描述多个 Schema 的联合。当你需要schema.Arrayschema.Values提供的多态行为、但目标字段并非集合时使用。

7.1 参数

  • definition必填。输入对象中嵌套实体的定义映射("schema 名 → Schema")。
  • schemaAttribute必填。每个实体上决定采用哪个 Schema 的属性。可为字符串或函数;若为函数,依次接收valueparentkey

Array/Values不同,UnionschemaAttribute没有默认值——源码(src/schemas/Union.js)在构造时若未提供会直接抛出Expected option "schemaAttribute" not found on UnionSchema.

7.2 实例方法

  • define(definition):合并 definition。

7.3 用法

注意:如果数据中出现了你没有提供映射的对象,原始对象会原样出现在 result 中,且不会为其创建实体

const data = { owner: { id: 1, type: 'user', name: 'Anne' } }; const user = new schema.Entity('users'); const group = new schema.Entity('groups'); const unionSchema = new schema.Union( { user: user, group: group }, 'type' ); const normalizedData = normalize(data, { owner: unionSchema });

输出:

{ entities: { users: { '1': { id: 1, type: 'user', name: 'Anne' } } }, result: { owner: { id: 1, schema: 'user' } } }

注意result.owner同样被包装为{ id: 1, schema: 'user' }schema字段记录判别属性值,denormalize时据此选择users还是groups还原。

八、schema.Values(definition, schemaAttribute):键值映射归一化

Values(definition, schemaAttribute)描述一个"值遵循给定 Schema"的 Map,适用于按任意键组织的对象(如 ID 字典)。

8.1 参数

  • definition必填。可以是单一 Schema(表示所有值都是该 Schema),也可以是映射(表示值的类型不唯一)。
  • schemaAttribute可选(当definition为映射时必填)。判别属性,可为字符串或函数;函数依次接收valueparentkey

8.2 实例方法

  • define(definition):合并 definition。

8.3 用法一:单一 Schema

const data = { firstThing: { id: 1 }, secondThing: { id: 2 } }; const item = new schema.Entity('items'); const valuesSchema = new schema.Values(item); const normalizedData = normalize(data, valuesSchema);

输出:

{ entities: { items: { '1': { id: 1 }, '2': { id: 2 } } }, result: { firstThing: 1, secondThing: 2 } }

result保留了原对象的键(firstThingsecondThing),值被替换为实体 ID。这正对应ValuesSchema.normalize的实现(src/schemas/Values.js):用reduce遍历Object.keys(input),对非空值调用normalizeValue,保持键不变。

8.4 用法二:多类型映射

当对象的值存在多种实体类型、且无法仅凭键名确定 Schema 时,使用映射方式(与schema.Unionschema.Array的多态用法一致):

注意:如果数据中出现了你没有提供映射的对象,原始对象会原样出现在 result 中,且不会为其创建实体

const data = { '1': { id: 1, type: 'admin' }, '2': { id: 2, type: 'user' } }; const userSchema = new schema.Entity('users'); const adminSchema = new schema.Entity('admins'); const valuesSchema = new schema.Values( { admins: adminSchema, users: userSchema }, (input, parent, key) => `${input.type}s` ); const normalizedData = normalize(data, valuesSchema);

输出:

{ entities: { admins: { '1': { id: 1, type: 'admin' } }, users: { '2': { id: 2, type: 'user' } } }, result: { '1': { id: 1, schema: 'admins' }, '2': { id: 2, schema: 'users' } } }

九、多态机制的底层统一实现

ArrayUnionValues的多态行为全部由PolymorphicSchema(src/schemas/Polymorphic.js)统一支撑,理解它即可举一反三:

  • _schemaAttribute:字符串形式的schemaAttribute会被包装为(input) => input[schemaAttribute],函数形式则原样保存(src/schemas/Polymorphic.js);
  • isSingleSchema:当未提供schemaAttribute时为true,此时normalizeValue直接按单一 Schema 处理;
  • inferSchema:多态模式下,用getSchemaAttribute(value, parent, key)求出判别值,再从定义映射中取出对应 Schema;若取不到(未提供映射),normalizeValue直接返回原始值,不创建实体——这正是文档中反复出现的"未提供映射的对象原样返回"警告的根源(src/schemas/Polymorphic.js);
  • 多态归一化结果统一包装为{ id: normalizedValue, schema: attr }denormalizeValue再据此还原(src/schemas/Polymorphic.js)。

十、从 API 到实战:与 Quick Start 的衔接

本文是 docs/api.md 的完整展开,与其配套的还有 docs/quickstart.md(以博客文章为例演示 Schema 组合)、docs/introduction.md 与 docs/faqs.md(常见问题)等文档。仓库还提供了可直接运行的示例:

  • examples/github/:GitHub API 响应归一化示例,配套schema.jsoutput.json
  • examples/relationships/:带关系实体的归一化输入/输出对照;
  • examples/redux/:与 Redux 集成的完整示例,涵盖 actions、modules、selectors,可对照docs/api.mdnormalize/denormalize在真实状态管理中的用法。

测试方面,src/tests/index.test.js 与 src/schemas/tests/ 下的Array.test.jsEntity.test.jsObject.test.jsUnion.test.jsValues.test.js为每个 API 提供了行为级验证,typescript-tests/ 目录则给出了各 Schema 的 TypeScript 类型用法参考,可与本文示例互相印证。

总结

掌握normalize/denormalize与五种 Schema 是使用 normalizr 的全部基础:Entity定义实体与嵌套关系(配合idAttributemergeStrategyprocessStrategyfallbackStrategy四个选项微调行为),Array/Object/Values覆盖三种容器结构,Union补足非集合多态,而define方法让循环引用 Schema 成为可能。结合本文对 src/index.js、src/schemas/ 各实现文件的源码剖析,你不仅能准确预测每一次归一化的输出,还能理解多态包装、缺失实体兜底、Immutable 兼容与防循环缓存等机制,从而在实际项目中设计出稳妥、可维护的数据层。

【免费下载链接】normalizrNormalizes nested JSON according to a schema项目地址: https://gitcode.com/gh_mirrors/no/normalizr

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询