createEntityAdapter
概览
一个能创建一集合关于在 范式化状态结构 执行 CRUD 操作的预设 reducers 和 selectors 的函数,其包含有某种特定数据对象的实例。这些 reducer 函数可以作为 case reducers 传入 createReducer
和 createSlice
。它们也可以被用作在 createReducer
和 createSlice
中的 “mutating" helper 函数。
这个 API 是从 NgRx 维护者创建的 the @ngrx/entity
library 引入的,但是为契合 Redux 工具包 而被大幅度地改造过。我们特此感谢 NgRx 团队,感谢他们是这个的 API 的原作者同时也允许我们引入并且根据我们的需要进行改良。
注意: "Entity" 这个术语指代的是在一个应用中一种独一无二的数据对象。例如,在一个博客应用中,你可能会有
User
,Post
, 和Comment
数据对象,其中他们每个都有许多实例存储在客户端和保持在服务端中。User
是一个 "实体" - 一种在一个应用当中的唯一数据对象类型。每一个实体的独一无二的实例都被假定有一个在某一个具体的字段的唯一 ID 值。
正如所有 Redux 逻辑一样,只有 纯 JS 对象和数组能被传到 store 中 - 没有类实例!
为了捋清这个说法的目的,我们将会使用
Entity
来指代那些被在 Redux 状态树中的某一部分 reducer 逻辑的拷贝管理着的特定数据类型,同时entity
用来指代单个这个类型的实例。例子:在state.users
中,Entity
代表着User
类型,而state.users.entities[123]
就是单个entity
。
这些被 createEntityAdapter
创建出来的方法,全都会操纵着一个 “实体状态” 结构,形如:
createEntityAdapter
可以在一个应用中被多次调用。如果你结合纯 JavaScript 一起使用的话,你有可能可以复用某个带有多实体类型的单独 adapter 定义,如果他们足够小的话(例如他们都有一个 entity.id
字段)。就 TypeScript 的使用而言,你会需要为了每一个唯一的 Entity
类型,而在不同的时间点调用 createEntityAdapter
,这样类型定义就会被正确地推断出来。
样本用法:
- TypeScript
- JavaScript
参数
createEntityAdapter
接受一个选项对象参数,该对象参数包括两个可选的字段。
selectId
一个接受一个单独 Entity
实例的函数,并且返回任何唯一的 ID 字段内的值。如果没有提供这个字段,默认的实现是 entity => entity.id
。如果你的 Entity
类型的把其唯一 ID 值存储在 entity.id
之外的字段,你 必须 提供一个 selectId
函数。
sortComparer
一个接受两个 Entity
实例的回调函数,且会返回标准的 Array.sort()
数字类型结果 (1, 0, -1) ,以示它们排序的相对顺序。
如果提供了,state.ids
数组会根据实例对象的比较,以被排好的顺序保留,这样的话映射 ID 数组以通过读取 ID 的形式来获取这些实体,会得到一个排好序的实体数组。
如果么有提供,state.ids
不会被排序,且关于顺序也不能得到保证。换句话说,state.ids
与标准 JavaScript 数组有几乎相同的特性。
返回值
一个 "entity adapter" 实例。一个 entity adapter 是一个普通 JS 对象(并非一个类), 包含有被生成的 reducer 函数、原始的 selectId
和 sortComparer
回调函数、 一个生成初始 "entity state" 值的方法,还有一些用来生成一组全局化和非全局化 selectors 函数的函数。
这个 adapter 实例会包含一下这些方法(额外的 TypeScript 类型也包括了):
CRUD 函数
一个实体 adapter 的核心内容就是一个 reducer 函数的集合,用于增添、更新以及从实体状态对象中删除实体实例对象:
addOne
: 接受一个单独实体,并且把其加进来。addMany
: 接受一个实体数组,或者一个形如Record<EntityId, T>
的对象,并且把它们添加进来。setAll
: 接受一个实体数组,或者一个形如Record<EntityId, T>
的对象,并且把它们添加进来,并且已有的实体内容用数组里面的值替换掉。removeOne
: 接受一个单独实体的 ID 值,并且移除已有的带有相同 ID 的实体。removeMany
: 接受一个带有实体 ID 值的数组,并且移除每一个已有的带有相同 ID 的实体。updateOne
: 接受一个 "update object",其包含有一个实体 ID 还有一个带有一个或多个新的字段值用于更新在changes
字段中做更新操作的对象,并且在对应的实体中做浅更新。updateMany
: 接受一个 update objects 的数组,并且对相应的实体做浅更新。upsertOne
: 接受一个单独实体。如果一个带有某个 ID 的实体已经存在,它会进行一次浅更新,且指定的字段会被合并到已有的实体中,其中任何匹配上的字段都会重写已有值。如果该实体不存在,那么它会被添加进来。upsertMany
: 接受一个实体数组,或者一个形如Record<EntityId, T>
的对象,并且把它们进行更新和插入进来。
每一个方法的函数签名长这样:
换句话说,这些方法接受一个 state 对象,形如 {ids: [], entities: {}}
,然后计算并返回一个新的状态对象。
这些 CRUD 方法有可能会通过如下几种方式被使用起来:
- 它们可以被当作 case reducers 直接传入到
createReducer
和createSlice
。 - 当手动调用的时候,它们可以被当作 "mutating" helper 方法来使用,例如一个在已有的 case reducer 中单独手写的
addOne()
方法,如果state
参数是一个 ImmerDraft
值的话。 - 当手动调用的时候,它们可以被当作 immutable 更新方法,如果
state
参数是一个普通 JS 对象或者数组的话。
注意: 这些方法 没有 对应的被创建出来 Redux actions - 它们只是单独的 reducers / update 逻辑. 这完全取决于你在哪里以及如何使用这些方法! 大多数情况下, 你依然想要把它们传入到
createSlice
或者在另外 reducer 中使用它们。
每一个方法都会检查 state
参数是不是一个 Immer Draft
。如果它是一个 draft 类型,这些方法会假定继续更改这些 draft 是安全的。如果不是,这些方法会把普通 JS 值传到 Immer 的 createNextState()
,然后返回不可更改的更新后的结果。
argument
可以是一个普通的值(比如一个单独的传到 addOne()
的 Entity
对象,或者是一个传到 addMany()
的 Entity[]
数组),又或者是一个跟 action.payload
有相同的值的 PayloadAction
action 对象。这样的话,我们就可以同时使用 helper 函数和 reducers。
浅更新的注意事项:
updateOne
,updateMany
,upsertOne
, 还有upsertMany
仅通过一个可变的方式进行浅更新。这意味着如果你的 update/upsert 操作是由一个嵌套属性组成的对象,新的值会重写 整个 已有的嵌套对象。对于你的应用用来说,这个有可能不是你想要的。总的原则是,这些方法最好配合 没有 嵌套属性的 normalized data 一起使用。
getInitialState
返回一个全新的形如 {ids: [], entities: {}}
的状态对象。
它接受一个可选的对象作为参数。这个对象里面的字段会被合并到返回的初始 state 值中去。例如,或许你想让你的分片也能追踪到一些加载状态:
Selector 函数
实体 adapter 会包含一个 getSelectors()
函数,其返回一组 selectors 函数,这些 selectors 知道如何读去实体 state 对象的内容:
selectIds
: 返回state.ids
数组。selectEntities
: 返回state.entities
查找表。selectAll
:映射state.ids
数组,并且以同样的顺序返回一个实体数组。selectTotal
:返回存储到 state 的实体总数。selectById
: 基于 state 和实体 ID,返回带有该 ID 的实体或者是undefined
每一个 selector 函数都会使用 Reselect 中的 createSelector
函数,以开启缓存计算结果的功能。
因为每一个 selector 函数取决于在状态树中,某个具体的实体状态对象所处的位置,getSelectors()
有两种调用方式:
如果调用时没有任何的参数,它会返回一个 "非全局化" 的 selector 函数的集合,其假定它的
state
参数是真正的实体状态对象。它也可以配合使用一个接受整个 Redux 状态树的 selector 函数进行调用,并且返回正确的实体状态参数。
例如,一个 Book
类型的实体状态在 Redux 状态树被存储为 state.books
。你可以使用 getSelectors()
,以两种方式读取该状态:
注意事项
应用多次更新操作
如果以多个针对与同一个 ID 的更新操作来调用 updateMany()
的话,它们会被合并到一次单独的更新,其中后面的操作会重新前面的。
对于 updateOne()
和 updateMany()
来说,改变一个已有实体的 ID 来匹配第二个已有实体的 ID 会造成第一个实体完全替换掉第二个的情况。
Examples
Exercising several of the CRUD methods and selectors: