SchemaSimple
SchemaSimple 是每个 schema 都要实现的接口。你可以自己实现它,告诉 @data-client/rest 如何对内置 schema 无法表达的值进行规范化、反规范化和查询。
大多数应用都用不到它,所以请先查看 Schema 概览。只有当你需要内置 schema 所不具备的运行时逻辑时,才考虑自定义 schema,例如输出依赖于 endpoint 参数,或者需要对较深的 Entity 图进行有限深度的遍历。
用法
这个 schema 会存储某个字段的所有翻译,然后只把组件所请求的 locale
对应的那一个交给组件:
import { Entity, RestEndpoint } from '@data-client/rest';
import type { IDenormalizeDelegate } from '@data-client/rest';
const localeKey = (args: readonly any[]) => args[0]?.locale;
class LocalizedText {
normalize(input: Record<string, string>) {
return input;
}
denormalize(
input: Record<string, string>,
delegate: IDenormalizeDelegate,
) {
const locale = delegate.argsKey(localeKey) ?? 'en';
return input[locale] ?? input.en;
}
queryKey() {
return undefined;
}
}
class Product extends Entity {
id = '';
name = '';
static key = 'Product';
static schema = {
name: new LocalizedText(),
};
}
const getProduct = new RestEndpoint({
path: '/products/:id',
searchParams: {} as { locale?: string },
schema: Product,
});
useSuspense(getProduct, { id: '5', locale: 'fr' }) 会返回一个 Product,其
name 为法语字符串,而 store 中仍保留所有语言。
delegate.argsKey() 告诉缓存输出依赖于 locale,因此切换语言时会重新计算该值。如果直接读取 delegate.args,会得到过时的结果。选择器必须是一个稳定的函数引用,因此应在模块作用域中定义它,或者在 schema 实例上只定义一次。
成员
normalize(input, parent, key, delegate, parentEntity?)
把该位置上的原始响应值转换为存储在
endpoint 结果中的内容。要规范化嵌套的 schema,请调用 delegate.visit(),而不要直接调用它们的方法。
normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate) {
return {
...input,
data: delegate.visit(this.schema, input.data, input, 'data'),
};
}
对于 schema 为 User 的包装 schema,
{ data: { id: '5', name: 'Ada' }, requestId: 'abc' } 这样的响应会被存储为
{ data: '5', requestId: 'abc' },而 User 则存放在 Entity 表中。
normalize() 只会对对象输入运行。对于没有 pk 的 schema,原始值会原样透传,除非它设置了 acceptsPrimitives = true,因此包裹 Entity 的包装 schema 会按 API 发送的原样存储裸 id。(单独的
Entity 会把真值 id 存储为字符串,所以 5 会变成 '5'。)同样,denormalize() 也永远不会收到 null 或 undefined。
parentEntity 是最近的外层 Entity schema(即该字段所属的类),如果有的话。大多数 schema 会忽略它;Scalar 用它来找到所绑定的 Entity。
denormalize(input, delegate)
接收 normalize() 的返回值,并构建由 hook 和
Controller 返回的值。对于嵌套的 schema,请调用
delegate.unvisit()。
denormalize(input: any, delegate: IDenormalizeDelegate) {
return {
...input,
data: delegate.unvisit(this.schema, input.data),
};
}
queryKey(args, unvisit, delegate)
构建在不获取数据、直接从 store 读取该 schema 时用于查找的规范化值,例如使用 useQuery()、
Controller.get 或 Query 时。它的结构通常与 normalize() 的返回值一致;unvisit 用于向嵌套 schema 获取它自己的查询键。
queryKey(args: readonly any[], unvisit: (schema: any, args: readonly any[]) => any) {
const data = unvisit(this.schema, args);
return data === undefined ? undefined : { data };
}
当 store 中的数据不足以给出结果时返回 undefined;当已知缓存结果无效时返回 delegate.INVALID。
Delegate
INormalizeDelegate
传给 normalize()。
| 成员 | 说明 |
|---|---|
visit(schema, value, parent, key) | 使用嵌套 schema 规范化 value |
args | Endpoint 参数 |
meta | 响应的 { fetchedAt, date, expiresAt } |
getEntity(key, pk) | 读取一个已存储的 Entity |
getEntities(key) | 读取某一类型的所有已存储 Entity |
mergeEntity(schema, pk, entity) | 通过 Entity 的合并生命周期来存储它 |
setEntity(schema, pk, entity, meta?) | 存储一个 Entity,替换原有内容 |
invalidate(schema, pk) | 将 Entity 标记为无效,使需要它的组件挂起 |
checkLoop(key, pk, input) | 若该输入在本次调用中已作为 (key, pk) 规范化过,则为 true;此时应停止递归 |
从 getEntity 到 invalidate 这些成员,只有类 Entity 的 schema 才需要。
IDenormalizeDelegate
传给 denormalize()。
| 成员 | 说明 |
|---|---|
unvisit(schema, input) | 使用嵌套 schema 反规范化 input |
argsKey(fn) | 返回 fn(args),并在该值变化时重新计算输出 |
args | Endpoint 参数。不会追踪变化;当输出依赖于参数时请使用 argsKey() |
IQueryDelegate
传给 queryKey()。
| 成员 | 说明 |
|---|---|
getEntity(key, pk) | 读取一个已存储的 Entity |
getEntities(key) | 读取某一类型的所有已存储 Entity |
getIndex(key, index, value) | 通过 Entity 索引查找 pk |
INVALID | 返回它以将结果标记为无效 |
类 Entity 的 schema
任何带有 pk 成员的 schema 都会被视为 Entity:它会按 key 和 pk 存储并记忆化,在循环引用中去重,并受
maxEntityDepth 限制。此外它还必须提供
key、createIfValid() 和 denormalize()。与其自己实现这些,不如直接继承 Entity。
示例:限制深度的关联关系
较深的双向图(Department ↔ Building ↔ Room)会让反规范化开销很大。推荐的解决方案是 Lazy,而
maxEntityDepth 可以限制 Entity 的总嵌套深度;自定义 schema 则可以针对每个关联关系限制遍历,精确解析 N
层。
DepthLimited 最多解析某个关联关系的 maxDepth 层,之后改为返回
pk。整个反规范化调用共享同一个 delegate,因此以它为键的
WeakMap 可以保存每次调用的状态。
import { Entity } from '@data-client/rest';
import type {
IDenormalizeDelegate,
INormalizeDelegate,
Schema,
} from '@data-client/rest';
class DepthLimited<S extends Schema> {
private readonly _state = new WeakMap<
IDenormalizeDelegate,
{ depth: number }
>();
constructor(
readonly schema: S,
readonly maxDepth: number,
) {}
normalize(
input: any,
parent: any,
key: any,
delegate: INormalizeDelegate,
) {
return delegate.visit(this.schema, input, parent, key);
}
denormalize(input: {}, delegate: IDenormalizeDelegate) {
let cell = this._state.get(delegate);
if (!cell) {
cell = { depth: 0 };
this._state.set(delegate, cell);
}
cell.depth++;
try {
if (cell.depth > this.maxDepth) return input;
return delegate.unvisit(this.schema, input);
} finally {
cell.depth--;
}
}
queryKey(): undefined {
return undefined;
}
}
class Department extends Entity {
id = '';
name = '';
static key = 'Department';
static schema = {
children: new DepthLimited([Department], 3),
parent: new DepthLimited(Department, 1),
};
}
反规范化后的 Entity 是按 Entity 记忆化的,而不是按深度。如果某个 Entity 第一次是在超过 maxDepth 的位置被访问到的,它会在该关联关系仍为 pk 的状态下被缓存,之后从同一个 store 直接读取它时,也会返回这个被截断的形式。
关于可检测循环的变体,以及它与 Lazy 之间的取舍,请参阅讨论
#3828。