跳到主要内容

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 成员。

multi-column primary key
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}​

指定 Entity.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;
  }
}

更多演示

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 自身的元数据计算。

默认使用第一个参数在 pk() 和 indexes 中查找

getEntity(key, pk?)​

传入一个参数时获取某一类型的所有 Entity,传入两个参数时获取单个 Entity

One argument
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
Two arguments
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?​

在规范化和反规范化期间都会运行。返回字符串表示出错(该字符串就是错误消息)。

在规范化期间,校验失败会导致该次获取产生错误。

在反规范化期间,校验失败会把该结果标记为“无效”,从而阻塞直到获取到结果。

默认仅在开发模式下做一些基本的字段存在性检查。可以覆盖它来禁用或定制。

为字段不完整的 endpoint 使用校验