Collection
Collections 用于定义可变的列表(Array)或映射(Values)。
这意味着它们可以增长和缩减。你可以用 .push 或 .unshift 向 Collection(Array) 添加元素,用 .remove 从 Collection(Array) 中移除元素,用 .assign 向 Collections(Values) 添加元素,并用 .move 在集合之间移动元素。
使用 Collections 时,RestEndpoint 提供了 .push, .unshift, .assign, .remove, .move
和 .getPage/ .paginated() 扩展器
用法
import React from 'react'; import { useController } from '@data-client/react'; import { getTodos } from './api/Todo'; export default function NewTodo({ userId }: { userId?: string }) { const ctrl = useController(); const [unshift, setUnshift] = React.useState(false); const handlePress = async e => { if (e.key === 'Enter') { const createTodo = unshift ? getTodos.unshift : getTodos.push; ctrl.fetch(createTodo, { title: e.currentTarget.value, userId, }); e.currentTarget.value = ''; } }; return ( <div className="listItem nogap"> <TextInput size="small" onKeyDown={handlePress} /> <label> <input type="checkbox" checked={unshift} onChange={e => setUnshift(e.currentTarget.checked)} />{' '} unshift </label> </div> ); }
结合 Values 使用 Collection
当 API 返回的是带键的对象而非数组时,将 Collection 与 Values 结合使用,即可对结果进行变更。
import { Entity, resource, Collection, Values } from '@data-client/rest';
class Stats extends Entity {
product_id = '';
volume = 0;
price = 0;
pk() {
return this.product_id;
}
static key = 'Stats';
}
export const StatsResource = resource({
urlPrefix: 'https://api.exchange.example.com',
path: '/products/:product_id/stats',
schema: Stats,
}).extend({
getList: {
path: '/products/stats',
// Collection wraps Values to enable .push, .assign, etc.
schema: new Collection(new Values(Stats)),
process(value) {
// Transform nested response structure
Object.keys(value).forEach(key => {
value[key] = {
...value[key].stats_24hour,
product_id: key,
};
});
return value;
},
},
});
这样就可以用 .assign 添加或更新条目。body 是一个对象,其键为集合中的键,值为要合并的 Entity 数据:
// Local-only update with ctrl.set()
ctrl.set(StatsResource.getList.schema.assign, {}, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});
// Network request with ctrl.fetch() - see RestEndpoint.assign
await ctrl.fetch(StatsResource.getList.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});
选项
argsKey 和 nestKey 用于计算 Collection 的 pk。当 Collection 作为顶层
endpoint 结果被规范化时使用 argsKey;当同一个 Collection 嵌套在
Entity 中时使用 nestKey。两者都提供,就能在这两种场景中复用同一个 Collection 定义。
argsKey(...args): Object
返回一个可序列化的对象,其成员根据 Endpoint 参数唯一确定这个集合。
import { RestEndpoint, Collection } from '@data-client/rest';
const userTodos = new Collection([Todo], {
argsKey: (urlParams: { userId?: string }) => ({
...urlParams,
}),
nestKey: (parent: { id: string }) => ({
userId: parent.id,
}),
});
const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: userTodos,
});
省略时,argsKey 默认为 params => ({ ...params })。
nestKey(parent, key): Object
返回一个可序列化的对象,其成员根据该集合所嵌套的父级唯一确定这个集合。
嵌套 Collection 的 pk 通常最好由它所嵌套的对象来定义。这样当嵌套的 Collection 实例的键具有相同值时,它们就能共享状态。当 argsKey 和 nestKey 返回相同结构的对象时,顶层读取和嵌套读取会解析到同一份集合状态。
import { Entity } from '@data-client/rest';
import { Todo, userTodos } from './Todo';
class User extends Entity {
id = '';
name = '';
username = '';
email = '';
todos: Todo[] = [];
static key = 'User';
static schema = {
todos: userTodos,
};
}
这种情况下,user.todos 与 argsKey 示例中 getTodos() 的响应始终是同一个(引用相等的)数组。在共享的 Collection 定义中同时添加这两个键函数:
const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});
nonFilterArgumentKeys?
argsKey 的一种便捷替代方案
nonFilterArgumentKeys 定义了一个判断条件,用于确定哪些参数键
_不_用于筛选结果。例如,如果你的 API 使用
'orderBy' 来选择排序方式——这个参数并不会影响响应中包含哪些 Entity。
const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys(key) {
return key === 'orderBy';
},
}),
});
为了方便,你也可以使用 RegExp 或字符串列表:
const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys: /orderBy/,
}),
});
const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys: ['orderBy'],
}),
});
这种情况下,author 和 group 被视为“筛选”参数键,这意味着它们会影响新创建的条目是否应被添加到这些列表中。而调用 push 时,orderBy 则不需要匹配。
import { Entity, Query, Collection, RestEndpoint } from '@data-client/rest'; class Post extends Entity { id = ''; title = ''; group = ''; author = ''; } export const getPosts = new RestEndpoint({ path: '/:group/posts', searchParams: {} as { orderBy?: string; author?: string }, schema: new Query( new Collection([Post], { nonFilterArgumentKeys: /orderBy/, }), (posts, { orderBy } = {}) => { if (orderBy) { return [...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy])); } return posts; }, ) });
createCollectionFilter?
为 addWith()、
push、unshift 和 assign 设置默认的 createCollectionFilter。
这些创建 schema 会用它来确定要添加到哪些集合中。
默认值:
createCollectionFilter(...args: Args) {
return (collectionKey: Record<string, string>) =>
Object.entries(collectionKey).every(
([key, value]) =>
this.nonFilterArgumentKeys(key) ||
// strings are canonical form. See pk() above for value transformation
`${args[0][key]}` === value ||
`${args[1]?.[key]}` === value,
);
}
方法
这些创建/移除 schema 可以与 Controller.set() 一起使用,进行不发起网络请求的纯本地更新。基于网络的变更请参阅 RestEndpoint 的专用扩展器。
push
一种创建 schema,将新条目放到该集合的_末尾_。
// Add a new todo to the end of the list (local only, no network request)
ctrl.set(getTodos.schema.push, { userId: '1' }, { id: '999', title: 'New Todo' });
unshift
一种创建 schema,将新条目放到该集合的_开头_。
// Add a new todo to the beginning of the list (local only)
ctrl.set(getTodos.schema.unshift, { userId: '1' }, { id: '999', title: 'New Todo' });
remove
一种按值从集合中移除条目的 schema。
Entity 值会被规范化以提取其 pk,然后与集合成员进行匹配。条目会从所有与所提供参数匹配的集合中移除(由 createCollectionFilter 筛选)。
// Remove from collections matching { userId: '1' } (local only)
ctrl.set(getTodos.schema.remove, { userId: '1' }, { id: '123' });
// Remove from all collections (empty args matches all)
ctrl.set(getTodos.schema.remove, {}, { id: '123' });
如需基于网络、同时更新 Entity 的移除操作,请参阅 RestEndpoint.remove。
move
一种在集合之间移动条目的 schema。它会把 Entity 从与其_现有_状态匹配的集合中移除,并添加到与该 Entity _新_状态(由最后一个参数得出)匹配的集合中。
它同时适用于 Collection(Array) 和 Collection(Values)。
// Move todo from userId '1' collection to userId '2' collection (local only)
ctrl.set(
getTodos.schema.move,
{ id: '10', userId: '2', title: 'Moved todo' },
[{ id: '10' }, { userId: '2' }],
);
移除时的筛选会使用 store 中该 Entity 的现有值来确定它当前属于哪些集合。添加时的筛选则使用合并后的 Entity 值(现有值 + 最后一个参数)来确定它应被放到哪里。
基于网络的移动请参阅 RestEndpoint.move。
assign
一种创建 schema,将其成员赋值到 Collection(Values) 中。仅适用于包裹了 Values 的 Collection。
const getStats = new RestEndpoint({
path: '/products/stats',
schema: new Collection(new Values(Stats)),
});
// Add/update entries in a Values collection (local only)
ctrl.set(getStats.schema.assign, {}, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
'ETH-USD': { product_id: 'ETH-USD', volume: 500 },
});
addWith(merge, createCollectionFilter): CreationSchema
为该集合构造一个自定义的创建 schema。 push、unshift、assign 和 paginate 都使用了它
merge(collection, creation)
它会将值与现有集合合并
createCollectionFilter
这个函数用于确定要添加到哪些集合中。它使用 argsKey 或 nestKey 返回的对象来判断该集合是否应获得此 schema 新创建的值。
由于参数可能是 number 等可序列化类型,我们建议使用 == 比较,例如 '10' == 10
(...args) =>
collectionKey =>
boolean;
moveWith(merge): MoveSchema
为该集合构造一个自定义的移动 schema。它与 addWith 类似,但用于 move 操作。merge 函数控制 Entity 如何被添加到目标集合中,而移除行为则会根据集合类型(Array 或 Values)自动推导。
当你需要控制被移动条目的插入位置时(例如插到开头而不是追加到末尾),它会很有用。
merge(collection, moved)
控制被移动的 Entity 如何添加到其目标集合中。
导出的 unshift 合并函数会把条目放到开头:
import { Collection, unshift, type CollectionOptions } from '@data-client/rest';
import type { PolymorphicInterface } from '@data-client/endpoint';
class MyCollection<
S extends any[] | PolymorphicInterface = any,
Args extends any[] = any[],
Parent = any,
> extends Collection<S, Args, Parent> {
constructor(schema: S, options?: CollectionOptions<Args, Parent>) {
super(schema, options);
// Prepend moved items instead of appending
this.move = this.moveWith(unshift);
}
}
unshift(合并函数)
一个将传入条目放到集合_开头_的合并函数。可与 moveWith 或 addWith 搭配使用,以控制插入顺序。
import { unshift } from '@data-client/rest';
生命周期方法
static shouldReorder(existingMeta, incomingMeta, existing, incoming): boolean
static shouldReorder(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return incomingMeta.fetchedAt < existingMeta.fetchedAt;
}
返回 true 时,会在合并中调换传入 Entity 与 store 中 Entity 的参数顺序。在默认的合并方式下,这会让现有 Entity 的字段覆盖传入 Entity 的字段,而不是反过来。
static merge(existing, incoming): mergedValue
static merge(existing: any, incoming: any) {
return incoming;
}
static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue
static mergeWithStore(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
): any;
在规范化期间,如果处理后的 Entity 已存在于 store 中,就会调用 mergeWithStore()。
pk: (parent?, key?, args?, parentEntity?): pk?
当嵌套在 Entity 中且 nestKey 可用时,pk() 会调用它;否则调用 argsKey。随后它会序列化结果,作为 pk
字符串。
pk(
value: any,
parent: any,
key: string,
args: readonly any[],
parentEntity?: any,
) {
const obj =
parentEntity && this.nestKey
? this.nestKey(parent, key)
: this.argsKey(...args);
for (const key in obj) {
if (typeof obj[key] !== 'string') obj[key] = `${obj[key]}`;
}
return JSON.stringify(obj);
}