normalizr 数据规范化实战:用 Schema 将嵌套 JSON 转换为扁平化实体字典
normalizr 数据规范化实战用 Schema 将嵌套 JSON 转换为扁平化实体字典【免费下载链接】normalizrNormalizes nested JSON according to a schema项目地址: https://gitcode.com/gh_mirrors/no/normalizrnormalizr 是一个轻量但功能强大的数据规范化Normalization工具库你只需按数据形态声明一套 Schema它就能把 API 返回的深层嵌套 JSON 转换为以实体类型为键、以 ID 为键名的扁平字典结构并同时在result中保留原有的引用关系。本指南以本项目 docs/README.md 为主线完整覆盖安装、动机、快速上手、normalize/denormalize与五大 Schema 的 API 细节并结合仓库源码与示例examples/github、examples/relationships、examples/redux讲清底层原理读完后你将能够独立为任意嵌套 JSON 设计规范化方案并自如地在 Redux / Flux 应用中维护实体缓存。安装与引入normalizr 通过 npm 或 yarn 从 NPM 仓库安装项目中无任何运行时依赖package.json的dependencies字段为空yarn add normalizrnpm install normalizr安装后即可按 ES Module 方式引入核心 API 与 Schema 构造器源码导出见 src/index.jsimport { normalize, denormalize, schema } from normalizr;当前仓库版本为3.6.2见 package.jsonmain指向dist/normalizr.jsCommonJSmodule指向dist/normalizr.es.jsES Module并随包提供 TypeScript 类型声明index.d.ts。动机为什么嵌套 JSON 难以维护现实中绝大多数公开或私有 API 返回的都是深度嵌套的 JSON。直接在这种结构上开发 JavaScript 应用会非常吃力尤其是使用 Flux 或 Redux 这类状态管理方案的场景。典型的痛点包括数据冗余同一用户对象在多篇文章、多条评论中重复出现导致内存浪费与更新时的一致性问题更新困难要修改某个作者的名字不得不遍历整个嵌套树去找到所有出现位置比较与缓存失效嵌套对象难以做浅比较React 组件无法高效判断状态是否变化。normalizr 正是为解决这一系列问题而生的。解决方案Normalizr 的核心思想Normalizr 是一个小巧但强大的工具它接收一份 JSON 数据外加一份 Schema 定义返回以实体类型为键、以 ID 为键名的实体字典dictionaries同时用 ID 引用替换掉嵌套对象从而把数据变成扁平、去重、易于维护的结构。其输出固定为以下形状源码实现见 src/index.js{ result: ..., // 规范化后的引用结构ID 或 ID 数组 entities: { // 按 Schema key 分组的实体字典 articles: { 123: { ... } }, users: { 1: { ... }, 2: { ... } } } }一个直观的例子取自 docs/introduction.md下面两篇文章共享同一位作者[ { id: 1, title: Some Article, author: { id: 1, name: Dan } }, { id: 2, title: Other Article, author: { id: 1, name: Dan } } ];经过规范化后作者Dan只保留一份两篇文章的author字段都变为 ID 引用{ 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 } } } }快速开始规范化一篇博客文章以一个典型博客文章 API 响应为例见 docs/quickstart.md原始数据如下{ id: 123, author: { id: 1, name: Paul }, title: My awesome blog post, comments: [ { id: 324, commenter: { id: 2, name: Nicole } } ] }文章里嵌入了两种实体类型users作者、评论者和comments。借助各种schema可以一次性把三类实体全部拍平import { normalize, schema } from normalizr; // 定义 users schema const user new schema.Entity(users); // 定义 comments schema const comment new schema.Entity(comments, { commenter: user, }); // 定义 article schema const article new schema.Entity(articles, { author: user, comments: [comment], }); const normalizedData normalize(originalData, article);规范化后的normalizedData为{ result: 123, entities: { articles: { 123: { id: 123, author: 1, title: My awesome blog post, comments: [ 324 ] } }, users: { 1: { id: 1, name: Paul }, 2: { id: 2, name: Nicole } }, comments: { 324: { id: 324, commenter: 2 } } } }可以看到文章、用户、评论三种实体被分别收进entities下以 Schema key 命名的字典中result只保留最外层的文章 ID所有嵌套引用全部替换为 ID。这正是 normalizr 的核心价值。核心 APInormalize 与 denormalizenormalize(data, schema)按给定的 Schema 定义规范化输入数据data必填需要规范化的 JSON或普通 JS 对象schema必填一份 Schema 定义。用法示例见 docs/api.mdimport { 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);输出{ result: { users: [ 1, 2 ] }, entities: { users: { 1: { id: 1 }, 2: { id: 2 } } } }从源码看normalize首先校验输入必须是对象否则抛出Unexpected input given to normalize...异常随后初始化entities收集器与visitedEntities防循环集合并调用内部visit函数递归遍历整棵树见 src/index.js。denormalize(input, schema, entities)denormalize是normalize的逆操作根据 Schema 与实体字典把扁平结构还原为嵌套对象。它同时支持普通对象与 Immutable 数据作为entities来源。input必填需要反规范化的结果通常是normalize输出中result的值schema必填与生成input时一致的那份 Schema 定义entities必填以实体 Schema 名为键的对象也接受包含 Immutable 数据的对象。用法示例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);输出{ users: [{ id: 1 }, { id: 2 }]; }两点特别提醒谨慎使用反规范化过早地把数据还原成大型嵌套对象可能对 React 等应用造成性能影响参考 docs/api.md 中的 Special Note。递归引用的处理如果 Schema 与数据存在递归引用只有第一次出现的实体会被完整展开后续引用会以 ID 形式返回避免无限循环。从源码看denormalize内部通过getUnvisit建立了一个缓存对象cache在展开实体前先写入缓存占位再递归展开从而既防止循环引用死循环也保证同一实体只展开一次见 src/index.js。Schema 全家桶五大构造器详解schema命名空间下共提供五种 Schema 构造器Array、Entity、Object、Union、Values统一导出自 src/index.js。Array(definition, schemaAttribute)描述一组 schema的集合型 Schema。如果输入值不是数组而是对象规范化结果会是该对象所有值的数组。同样的行为可以用简写语法[mySchema]表达。definition必填数组包含的单一 schema或者schema 到属性值的映射表schemaAttribute可选当definition不是单一 schema 时必填用于决定每个实体按映射表中哪个 schema 规范化。可以是字符串或函数函数接收(value, parent, key)三个参数。实例方法define(definition)会把新定义与构造时传入的定义合并常用于构建循环引用的 Schema。单类型数组const data [{ id: 123, name: Jim }, { id: 456, name: Jane }]; const userSchema new schema.Entity(users); const userListSchema new schema.Array(userSchema); // 或使用简写语法 const userListSchema [userSchema]; const normalizedData normalize(data, userListSchema);输出{ entities: { users: { 123: { id: 123, name: Jim }, 456: { id: 456, name: Jane } } }, result: [ 123, 456 ] }多类型多态数组当数组元素不止一种实体时必须提供映射表与schemaAttribute。注意如果数据中出现映射表之外的实体该对象会原样保留在 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 中的每个元素会额外携带schema字段标记该 ID 属于哪种实体类型以便反规范化时找回正确的 Schema。这一逻辑实现在 src/schemas/Polymorphic.js通过inferSchema按schemaAttribute从定义映射中挑出对应 schema找不到映射时直接返回原值。Entity(key, definition {}, options {})最核心的 Schema 类型用于描述单一实体。key必填字符串该类型实体在规范化结果字典中使用的键名如usersdefinition实体内部嵌套实体的定义默认空对象。只需声明包含嵌套实体的字段其他字段会被原样拷贝进规范化后的实体optionsidAttribute实体唯一 ID 所在的属性接受字符串 key 或返回 ID 值的函数默认id。该函数可能被多次执行因此生成的 ID 必须每次一致——用uuid这类随机生成器会导致难以预料的错误。作为函数时接收(value, parent, key)参数mergeStrategy(entityA, entityB)当两个实体拥有相同 ID 时的合并策略默认把后出现的实体合并到先出现的实体上即{ ...entityA, ...entityB }见 src/schemas/Entity.jsprocessStrategy(value, parent, key)实体的预处理策略可用来追加数据、补默认值或彻底改写实体默认返回输入的浅拷贝。建议始终返回输入的副本不要修改原对象fallbackStrategy(key, schema)反规范化时遇到ID 引用了缺失实体的兜底策略接收(key, schema)参数返回一个替代实体。实例方法define(definition)同样用于合并定义、构建循环引用。实例属性包括key与idAttribute。基础用法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, // 将 entityB 的所有字段合并到 entityA 上但保留 entityA 的 favorites mergeStrategy: (entityA, entityB) ({ ...entityA, ...entityB, favorites: entityA.favorites }), // 从实体中剔除 url 字段 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 }idAttribute函数用法函数必须返回 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: (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] }fallbackStrategy用法下面的例子中第三本书author: 3在实体字典里并不存在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 }, } ]源码视角的 Entity 规范化流程见 src/schemas/Entity.js通过idAttribute计算实体 ID借助visitedEntities记录已访问的 (实体类型, ID, 输入对象)若同一输入对象被再次访问则直接返回 ID防止循环引用造成死循环调用processStrategy得到预处理后的实体副本遍历 schema 定义中声明了嵌套 schema 的字段对其中对象类型的值递归调用visit最后通过addEntity将处理后的实体写入entities字典——若 ID 已存在则调用mergeStrategy合并见 src/index.js。Object(definition)描述一个值需要被规范化为实体的普通对象映射。同样的行为可以用简写语法{ ... }表达。definition必填对象内嵌套实体的定义默认空对象。同样只需声明持有实体的字段其他字段原样拷贝到规范化输出。实例方法define(definition)用于合并定义。用法示例const data { users: [{ id: 123, name: Beth }] }; const user new schema.Entity(users); const responseSchema new schema.Object({ users: new schema.Array(user) }); // 或简写 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 的normalize实现中若某字段规范化结果为undefined或null该字段会从结果对象中被删除——这保证了不会在 result 中残留空引用。Union(definition, schemaAttribute)描述多种 Schema 的联合适用于在非集合字段单个字段上实现多态行为——即schema.Array或schema.Values的多态能力在单个引用字段上的版本。definition必填嵌套实体的映射表schemaAttribute必填决定按映射表中哪个 schema 规范化。可以是字符串或函数函数接收(value, parent, key)。同样支持define(definition)合并定义。若数据中某对象没有映射原对象会被原样保留在 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 } } }源码中UnionSchema强制要求提供schemaAttribute否则构造时直接抛错见 src/schemas/Union.js。Values(definition, schemaAttribute)描述值遵循给定 schema 的映射表Map即对象的所有 value 都会被规范化。definition必填单一 schema或 schema 到属性值的映射表schemaAttribute可选当definition不是单一 schema 时必填同Array/Union的用法。用法示例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 } }当值的类型不只一种、且无法从 key 推断 schema 时可像Union/Array一样使用映射表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 } } }ValuesSchema的 normalize 实现会跳过值为undefined/null的键见 src/schemas/Values.js。深入底层一次 normalize 调用发生了什么综合 src/index.js 与各 Schema 源码normalize的完整调用链如下入口normalize(input, schema)校验输入为对象初始化entities、addEntity、visitedEntities核心递归函数visit(value, parent, key, schema, ...)分发若 schema 是纯对象含数组简写形式[...]与对象简写{...}则调用ArrayUtils.normalize或ObjectUtils.normalize否则调用schema.normalize(...)EntitySchema.normalize计算 ID、防循环、预处理、递归访问嵌套字段、最后写入实体字典所有嵌套 schema 访问完毕后result与entities一并返回。denormalize则沿相反方向从result出发unvisitEntity通过缓存防止循环与重复展开对 Immutable 实体ImmutableUtils.isImmutable通过__ownerID特征识别见 src/schemas/ImmutableUtils.js并配合denormalizeImmutable以字符串 key 读写实体字段。实战示例从 GitHub 到 Redux仓库提供了三个可直接运行的示例Normalizing GitHub Issues以 GitHub 仓库的 issue / pull request 列表为输入演示多态数组的典型用法。核心 schema 定义见 examples/github/schema.jsissue与pullRequest都是包含user、assignee、assignees、labels、milestone等嵌套实体的Entity最后用一个schema.Array配合(entity) (entity.pull_request ? pullRequests : issues)的schemaAttribute函数做多态分发把混合列表拆分为issues与pullRequests两类实体Relational Data演示实体间多对多/一对多关系的规范化与反规范化输入输出样例见 examples/relationships/input.json 与 examples/relationships/output.jsonInteractive Redux一个可交互的 Redux 示例展示 normalizr 与 Redux 的完整集成方式——包含 API 层 schema 定义examples/redux/src/api/schema.js以及按资源拆分的 reducer 模块examples/redux/src/redux/modules。其集成思路与 docs/README.md 中为 Flux / Redux 场景设计的定位完全一致API 响应经normalize后写入 Redux store各实体类型按 key 分表维护组件层按需通过denormalize还原视图所需的数据。构建文件与模块格式normalizr 针对不同运行环境提供了多套构建产物详见 docs/introduction.md 的 Build Files 一节src/*CommonJS 未打包源码是配合自有打包器使用的推荐入口也是package.json中默认的指向normalizr.js、normalizr.min.jsCommonJS 格式normalizr.amd.js、normalizr.amd.min.jsAMDAsynchronous Module Definition格式normalizr.umd.js、normalizr.umd.min.jsUMDUniversal Module Definition格式normalizr.browser.js、normalizr.browser.min.jsIIFEImmediately-Invoked Function Expression格式适合浏览器中以独立script标签引入。需要说明的是官方并不推荐在浏览器中直接通过script srcnormalizr.js方式使用建议使用 webpack、rollup、browserify 等打包器引入以便获得 tree-shaking 与依赖管理能力。当前仓库的打包配置见 rollup.config.js同时提供了 TypeScript 类型测试typescript-tests以保证声明文件正确性。依赖、稳定性与维护状态零依赖normalizr 本身没有任何运行时依赖package.json的dependencies为空可放心引入任意项目稳定性项目长期处于稳定发布状态被大量项目使用而未出问题作者在仓库主 READMEREADME.md中声明该项目已不再积极维护——如果你需要新特性或发现 bug官方建议 fork 后自行维护。因此在选用时需要自行评估这一前提开源背景normalizr 最初由 Dan Abramov 创建受 Jing Chen 的启发v3 起由 Paul Armstrong 完全重写并持续维护。更多阅读Introduction动机、核心思路示例与 Build Files 详情Quick Start五分钟快速上手API 参考normalize/denormalize与全部 Schema 构造器的完整参数说明Using with JSONAPI如何配合 JSON:API 规范使用FAQ常见问题【免费下载链接】normalizrNormalizes nested JSON according to a schema项目地址: https://gitcode.com/gh_mirrors/no/normalizr创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →