normalizr API 完全指南:normalize / denormalize 与五大 Schema 的源码级解析
【免费下载链接】normalizrNormalizes nested JSON according to a schema项目地址: https://gitcode.com/gh_mirrors/no/normalizr
本篇技术指南以 normalizr 的官方 API 文档(docs/api.md)为骨架,系统讲解normalize、denormalize两个核心函数与Array、Entity、Object、Union、Values五种 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 定义(Entity、Array、Object、Union、Values或它们的嵌套组合,也支持字面量简写语法)。
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.normalize或ObjectUtils.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:通过检测对象上是否存在__ownerID(Immutable.Map)或_map.__ownerID(Immutable.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 | 无 |
Array、Union、Values三者共享多态基类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 详解
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
idAttribute | string|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 时,默认返回undefined;fallbackStrategy允许你生成一个占位实体,避免下游空指针:
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:unvisitEntity中getEntity(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.Array或schema.Values提供的多态行为、但目标字段并非集合时使用。
7.1 参数
definition:必填。输入对象中嵌套实体的定义映射("schema 名 → Schema")。schemaAttribute:必填。每个实体上决定采用哪个 Schema 的属性。可为字符串或函数;若为函数,依次接收value、parent、key。
与Array/Values不同,Union的schemaAttribute没有默认值——源码(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为映射时必填)。判别属性,可为字符串或函数;函数依次接收value、parent、key。
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保留了原对象的键(firstThing、secondThing),值被替换为实体 ID。这正对应ValuesSchema.normalize的实现(src/schemas/Values.js):用reduce遍历Object.keys(input),对非空值调用normalizeValue,保持键不变。
8.4 用法二:多类型映射
当对象的值存在多种实体类型、且无法仅凭键名确定 Schema 时,使用映射方式(与schema.Union和schema.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' } } }九、多态机制的底层统一实现
Array、Union、Values的多态行为全部由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.js与output.json; - examples/relationships/:带关系实体的归一化输入/输出对照;
- examples/redux/:与 Redux 集成的完整示例,涵盖 actions、modules、selectors,可对照
docs/api.md中normalize/denormalize在真实状态管理中的用法。
测试方面,src/tests/index.test.js 与 src/schemas/tests/ 下的Array.test.js、Entity.test.js、Object.test.js、Union.test.js、Values.test.js为每个 API 提供了行为级验证,typescript-tests/ 目录则给出了各 Schema 的 TypeScript 类型用法参考,可与本文示例互相印证。
总结
掌握normalize/denormalize与五种 Schema 是使用 normalizr 的全部基础:Entity定义实体与嵌套关系(配合idAttribute、mergeStrategy、processStrategy、fallbackStrategy四个选项微调行为),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),仅供参考