跳到主要内容

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>
  );
}
结果
Store▶

结合 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;
    },
  )
});
结果
Store▶

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);
}