EntityMixin
Entity 定义了单个 唯一 对象。
如果你已经为数据类型定义了类,EntityMixin 可能正适合你。
import { EntityMixin } from '@data-client/rest'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } export class ArticleEntity extends EntityMixin(Article) {}
选项
mixin 的第二个参数可以方便地定制构造方式。如果未指定,将使用 Base
类的静态成员。另外,与 Entity 一样,你也始终可以把它们指定为最终类的静态成员。
class User { username = ''; createdAt = Temporal.Instant.fromEpochMilliseconds(0); } class UserEntity extends EntityMixin(User, { pk: 'username', key: 'User', schema: { createdAt: Temporal.Instant.from }, }) {}
pk: string | (value, parent?, key?, args?) => string | number | undefined = 'id'
指定 Entity.pk
string 表示用作 pk 的字段。
function 的用法与 Entity.pk 相同,只是第一个参数(value)即 this
默认为 'id';这意味着 pk 是必需选项,除非 Base 类拥有可序列化的 id 成员。
class Thread { forum = ''; slug = ''; content = ''; } class ThreadEntity extends EntityMixin(Thread, { pk(value) { return [value.forum, value.slug].join(','); }, }) {}
key: string
指定 Entity.key
schema: {[k:string]: Schema}
const 与 class
如果你不需要进一步定制该 Entity,可以使用 const 声明,而不是
extend 出另一个类。
在 TypeScript 中引用 class token 时有一个细微差别——
class 声明指代的是实例类型;而 const tokens 指代的是值,因此你必须使用 typeof,但 typeof 得到的是类的类型,所以你还必须在外面再套一层
InstanceType。
import { schema } from '@data-client/rest'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } export class ArticleEntity extends EntityMixin(Article) {} export const ArticleEntity2 = EntityMixin(Article); const article: ArticleEntity = ArticleEntity.fromJS(); const articleFails: ArticleEntity2 = ArticleEntity2.fromJS(); const articleWorks: InstanceType<typeof ArticleEntity2> = ArticleEntity2.fromJS();
生命周期
要覆盖 process() 等生命周期方法,必须使用 class ... extends EntityMixin(...) {} 形式。
EntityMixin() 的选项只包括 pk、key 和 schema——生命周期的覆盖要写在类本身上。
import { EntityMixin } from '@data-client/rest'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } // ❌ Not supported (lifecycle methods are not EntityMixin options) // export const ArticleEntity = EntityMixin(Article, { // process(input) { // return input; // }, // }); // ✅ Use a class when adding lifecycle methods export class ArticleEntity extends EntityMixin(Article) { static process(input: any, parent: any, key: string | undefined, args: any[]) { const processed = super.process(input, parent, key, args); processed.tags ??= []; return processed; } }
static fromJS(props): Entity
把 props 复制到新实例的工厂方法。请用它代替 new MyEntity(),以确保默认 props 会被覆盖。
static process(input, parent, key, args): processedEntity
在规范化该 Entity 的开始阶段运行。返回值会保存到 store 中,并传给 pk()。
默认只是简单地复制响应({...input})
如何通过覆盖它来为关系型数据构建反向查找
缺少 id 的情况
import { EntityMixin } from '@data-client/rest';
class Stream {
username = '';
title = '';
game = '';
currentViewers = 0;
live = false;
}
class StreamEntity extends EntityMixin(Stream) {
static key = 'Stream';
static process(value, parent, key, args) {
// super.process creates a copy of value
const processed = super.process(value, parent, key, args);
processed.username = args[0]?.username;
return processed;
}
}
动态失效
从 Entity.process 返回 undefined
会使该 Entity 失效。这让我们可以根据具体的响应数据,动态地进行失效。
import { EntityMixin } from '@data-client/rest';
class PriceLevel {
price = 0;
amount = 0;
}
class PriceLevelEntity extends EntityMixin(PriceLevel) {
static process(
input: [number, number],
parent: any,
key: string | undefined,
): any {
const [price, amount] = input;
if (amount === 0) return undefined;
return { price, amount };
}
}
static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue
static mergeWithStore(
existingMeta: {
date: number;
fetchedAt: number;
},
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
const shouldUpdate = this.shouldUpdate(
existingMeta,
incomingMeta,
existing,
incoming,
);
if (shouldUpdate) {
// distinct types are not mergeable (like delete symbol), so just replace
if (typeof incoming !== typeof existing) {
return incoming;
} else {
return this.shouldReorder(
existingMeta,
incomingMeta,
existing,
incoming,
)
? this.merge(incoming, existing)
: this.merge(existing, incoming);
}
} else {
return existing;
}
}
当规范化过程中发现 store 中已存在某个处理过的 Entity 时,会调用 mergeWithStore()。
它会调用 shouldUpdate()、shouldReorder(),并可能调用 merge()
static shouldUpdate(existingMeta, incomingMeta, existing, incoming): boolean
static shouldUpdate(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return existingMeta.fetchedAt <= incomingMeta.fetchedAt;
}
阻止更新
shouldUpdate 也可以用来短路 Entity 的更新。
import deepEqual from 'deep-equal';
import { EntityMixin } from '@data-client/rest';
class Article {
id = '';
title = '';
content = '';
published = false;
}
class ArticleEntity extends EntityMixin(Article) {
static shouldUpdate(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return !deepEqual(incoming, existing);
}
}
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 会在 merge 中调换传入 Entity 与 store 中 Entity 的参数顺序。在默认的 merge 下,这会让已有 Entity 的字段覆盖传入 Entity 的字段,而不是反过来。
示例
import { EntityMixin } from '@data-client/rest'; class LatestPrice { id = ''; updatedAt = 0; price = '0.0'; symbol = ''; } class LatestPriceEntity extends EntityMixin(LatestPrice) { static shouldReorder( existingMeta: { date: number; fetchedAt: number }, incomingMeta: { date: number; fetchedAt: number }, existing: { updatedAt: number }, incoming: { updatedAt: number }, ) { return incoming.updatedAt < existing.updatedAt; } }
探索 coin-app 示例
static merge(existing, incoming): mergedValue
static merge(existing: any, incoming: any) {
return {
...existing,
...incoming,
};
}
Merge 用于处理传入的 Entity 已经存在的情况。当同一个响应中出现相同的 Entity 时,会直接调用它。默认情况下,当 mergeWithStore() 判定传入的 Entity 应与已持久化在 Reactive Data Client store 中的 Entity 合并时,也会调用它。
如何通过覆盖它来为关系型数据构建反向查找
static mergeMetaWithStore(existingMeta, incomingMeta, existing, incoming): meta
static mergeMetaWithStore(
existingMeta: {
expiresAt: number;
date: number;
fetchedAt: number;
},
incomingMeta: { expiresAt: number; date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return this.shouldReorder(existingMeta, incomingMeta, existing, incoming)
? existingMeta
: incomingMeta;
}
当规范化过程中发现 store 中已存在某个处理过的 Entity 时,会调用 mergeMetaWithStore()。
static queryKey(args, queryKey, getEntity, getIndex): pk?
这个方法让 Entities 成为 Queryable,从而无需 endpoint 即可访问 store。
覆盖它可以定制这一行为,或者完全禁用它。
返回 undefined 会禁止这一行为。
返回 pk 字符串时,会尝试查找该 Entity 并在响应中使用它。
使用时,过期策略会根据该 Entity 自身的元数据计算。
getEntity(key, pk?)
传入一个参数时获取某一类型的所有 Entity,传入两个参数时获取单个 Entity
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
if (getEntity(this.key, id)) return id;
getIndex(key, indexName, value)
返回索引条目(value->pk 映射)
const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];
static createIfValid(processedEntity): Entity | undefined
在反规范化 Entity 时调用。如果该 Entity 被认为是“有效”的,就会创建这个类的实例。
返回 undefined 会导致 Invalid 过期状态,与 Invalidate 类似。
Invalid 过期状态通常意味着 hook 会进入加载状态并尝试重新获取。
static createIfValid(props): AbstractInstanceType<this> | undefined {
if (this.validate(props)) {
return undefined as any;
}
return this.fromJS(props);
}
static validate(processedEntity): errorMessage?
在规范化和反规范化期间都会运行。返回字符串表示出错(该字符串就是错误消息)。
在规范化期间,校验失败会导致该次获取产生错误。
在反规范化期间,校验失败会把该结果标记为“无效”,从而阻塞直到获取到结果。
默认仅在开发模式下做一些基本的字段存在性检查。可以覆盖它来禁用或定制。