引擎与资源
ViewEngine 是宿主在应用启动时建一次、整个应用共用的对象:它登记定义(resources),握住保存视图的 store,管理打开着的运行时,并且是打开、新建、保存、改名、删除这些命令的唯一入口。之所以只有一个入口,是因为每次写入都走同一条路——默认界面和宿主自己搭的界面因此表现一致,任何不成功的结局都落进同样三种恢复动作(retryWrite、abandonWrite、resolveConflict)。
直接构造它,不要继承:它标了 @sealed,基类 EngineResources 只是文件的拆法,不是扩展点。
相关指南:视图引擎入门(第 6 步建出引擎与资源)、把视图引擎接进宿主(为什么整个应用只有一个引擎)、视图引擎的核心概念(引擎管理的视图与运行时)。
建一个引擎
每个资源把一份定义和它的数据来源配成一对;同一个来源上的所有页面共用查询、偏好与查询描述。MemoryViewStore 刷新就忘,开发与测试够用;在 Wow 服务上换成 WowViewStore。
import { MemoryViewStore, ViewEngine } from '@ahoo-wang/wow-view-engine';
import { browserRuntimeEnvironment } from '@ahoo-wang/wow-view-engine/react';
export const engine = new ViewEngine({
resources: [{ definition: ordersDefinition, source }],
store: new MemoryViewStore(),
// 失败照常在发生处显示;这里是给日志的一份。
environment: browserRuntimeEnvironment({
onError: ({ kind, error }) => console.error(`[view-engine] ${kind}`, error),
}),
});选项
| 选项 | 作用 |
|---|---|
resources | 宿主登记的全部资源:每份定义,以及它的数据来源。看板的定义没有来源——它自己不查询 |
store | 保存视图与偏好的端口(持久化端口) |
resolveOptions | reference 字段远端候选值的来源,按同一个键查 |
kinds | 字段类型登记表;缺省是内置类型(定义与字段类型) |
text | 定义里的键(text(key))在引擎起始语言下怎样说,给没有 Provider 的宿主;有 ViewHost 或 MessagesProvider 时由它们按自己的语言说。缺省时,在设定语言前键就读作键本身 |
limits | 宿主在 DEFAULT_RUNTIME_LIMITS 上的预算,没写的保持缺省。来源预算(maxPageSize、maxPageWindow、maxAnalysisRows、maxQueryFilterNodes、maxFilterValues)由来源的查询描述给出,这里给的只能往下压——除非要引擎比服务端允许的少要一些,否则别写 |
environment | 运行时与宿主之间唯一的接口:时钟、时区、计时器、页面是否可见、失败通知(RuntimeEnvironment) |
newId | 生成幂等键;测试里覆盖它,让键好读 |
onIssue | 没有调用方可拒绝、背后也没有抛出的发现:定义的准入、被丢掉的列表项、抛了错的变更监听。查询、存储、导出、渲染这些失败走 environment.onError。缺省时开发构建(NODE_ENV 为 development)把它们按资源分组写到控制台,附修正办法;其他构建丢弃 |
为什么分成 onIssue 和 onError 两路:前者是宿主写的声明有问题,修代码就好;后者是运行中遇到的失败,界面已经在原地说了,这一份是给监控的。
export interface ViewEngineOptions {
environment?: RuntimeEnvironment;
kinds?: FieldKindRegistry;
limits?: Partial<RuntimeLimits>;
newId?(): string;
onIssue?(issue: Issue): void;
resolveOptions?(key: string): OptionSource;
resources: readonly ViewResource[];
store: ViewStore;
text?(key: string): string | undefined;
}ViewResource
宿主登记的一项:一份定义,以及它的数据从哪里来。数据定义的 source 是行的来源;看板没有。
export interface ViewResource {
definition: ViewDefinition;
source?: ViewSource;
}数据来源
ViewSource 是 QueryApi 的三个方法,所以 wow-client 的快照查询客户端原样就满足它;别的后端或测试可以手写一个(测试用 /testing 的 memorySource)。
- 取消用的是
AbortController,而不是其他端口用的AbortSignal,因为QueryApi收的就是它,这个端口要让 wow-client 的查询客户端不经改写插进来。控制器归引擎:有更新的请求取代这一个、或运行时被释放时,引擎去 abort;手写的来源只读abortController?.signal,自己不 abort。 aggregate返回RecordData[],每个值都以unknown到达,经字段类型读取,而不是信任调用方断言的行类型。describe可选:来源的能力描述。引擎在第一个视图运行前读一次,按它收窄每份定义、取它的限额,之后按自己的节奏复查;带着已有版本问时,没变就答notModified。wow-client 的describeSnapshot或describeEventStream原样就合用。不提供时,视图只凭定义和缺省限额运行。
export interface ViewSource {
aggregate(query: AggregationQuery, attributes?: Record<string, unknown>, abortController?: AbortController): Promise<RecordData[]>;
cursor(query: CursorQuery, attributes?: Record<string, unknown>, abortController?: AbortController): Promise<CursorPage<RecordData>>;
describe?(previous?: string, attributes?: Record<string, unknown>, abortController?: AbortController): Promise<QueryDescriptorResult>;
paged(query: FilterPagedQuery, attributes?: Record<string, unknown>, abortController?: AbortController): Promise<PagedList<RecordData>>;
}接 Wow 服务的来源,见 Storybook 接入导览的 wowSource.ts:四个方法各转给快照查询客户端和查询描述客户端。
运行环境
运行时与宿主之间唯一的接口。相对日期、刷新计时、「有没有人在看」以及宿主想听的失败都经过它,所以 model 到 store 各层不碰 DOM 和系统时钟,测试也不必伪造全局对象就能驱动时间。浏览器里用 /react 的 browserRuntimeEnvironment(overrides?);无界面时用根入口的 defaultRuntimeEnvironment(overrides?)。
timeZone:解析相对日期与预设日期用的 IANA 时区。onError:引擎干活时遇到的每个失败——查询、存储调用、导出、渲染、图表、声明的操作所发的命令——各告诉一次。界面照常在原地答复,这是给宿主的副本;它抛出的东西被丢弃,不提供时哪里都不记。
export interface RuntimeEnvironment {
clearTimeout(handle: unknown): void;
now(): Date;
onError?(event: ViewErrorEvent): void;
setTimeout(callback: () => void, ms: number): unknown;
timeZone: string;
visibility: VisibilitySource;
}ViewEngine
下面是宿主最常用的成员;完整签名见 API 报告。默认界面已经替你调用了它们,自己搭界面或写测试时才直接用。
| 成员 | 作用 |
|---|---|
list(definitionId) | 代码里声明的系统视图在前,然后是存储里的。保留命名空间里的 id 被丢弃并报告:只有定义能声明系统视图。存储失败时 failed 说明原因,声明的系统视图仍在 items 里 |
open(instanceId, options?) | 打开一个保存的视图,或不碰存储地打开代码声明的视图。options.scopeFilter 与配置一起准入,从第一次查询起生效 |
create(definitionId, input) | 一个未保存的视图,带着完整配置(defaultRecordConfig 等生成)立刻执行。这里不问权限:首次 save 时才问 |
save(runtime)、saveAs(runtime, input) | 首次保存即创建,之后覆盖,都要求草稿没有错误。saveAs 的 scope: 'system' 是存储型系统视图,需要 editSystem |
rename、delete、changeAudience | 改名不带配置,所以草稿有错也不挡它。changeAudience 需要存储实现可选的同名方法,否则被拒(view.changeAudience.unsupported) |
publishAsSystem(id) | 「发布为系统视图」:把保存的视图(按已保存的样子,不是打开着的草稿)复制成存储型系统视图;需要 editSystem |
retryWrite、abandonWrite、resolveConflict | 三种恢复动作:按原 requestId 重放;放弃结局保留草稿;reload 取服务端状态、overwrite 以冲突报告的修订号重放(新的逻辑写,所以换新 requestId) |
close(runtime) | 关闭一个打开的视图:释放并移出登记。只调 runtime.dispose() 会让引擎握着一个死运行时 |
subscribe(listener) | 任一写入改变了某定义的列表时通知,返回取消订阅的函数 |
permissions(definitionId) | 存储给出的权限(ViewPermissions) |
setText(text, language?) | 换定义的键在哪种语言下怎样说;ViewHost 会替你调用 |
export declare class ViewEngine extends EngineResources {
constructor(options: ViewEngineOptions);
abandonWrite(target: WriteTarget): void;
changeAudience(id: string, audience: ViewAudience): Promise<ViewInstance>;
close(runtime: ViewRuntime): void;
create<C extends ViewConfig>(definitionId: string, input: CreateInput<C>): RuntimeFor<C>;
definitionIssues(definitionId: string): Issue[];
delete(id: string): Promise<void>;
dispose(): void;
readonly environment: RuntimeEnvironment;
readonly kinds: FieldKindRegistry;
readonly limits: RuntimeLimits;
list(definitionId: string): Promise<ViewListing>;
open(instanceId: string, options?: OpenOptions): Promise<AnyViewRuntime>;
openRuntimes(): readonly ViewRuntime[];
pendingWrites(): ReadonlyMap<string, WriteState>;
permissions(definitionId: string): ViewPermissions;
preferences(definitionId: string): Promise<ViewPreferences>;
publishAsSystem(id: string): Promise<ViewInstance>;
rename(id: string, title: string): Promise<ViewInstance>;
reorder(definitionId: string, order: string[]): Promise<ViewPreferences>;
resolveConflict(target: WriteTarget, choice: ConflictChoice): Promise<ViewInstance | ViewPreferences | void>;
retryWrite(target: WriteTarget): Promise<ViewInstance | ViewPreferences | void>;
save(runtime: ViewRuntime): Promise<ViewInstance>;
saveAs(runtime: ViewRuntime, input: { title: string; scope: ViewScope }): Promise<ViewInstance>;
setAutoRun(definitionId: string, autoRun: boolean): Promise<ViewPreferences>;
setDefault(definitionId: string, instanceId: string | null): Promise<ViewPreferences>;
readonly store: ViewStore;
subscribe(listener: ViewChangeListener): () => void;
}打开与新建的输入
scopeFilter:一个外层条件,用视图自己的字段名写,与配置一起准入——按客户限定的订单页不会让一个未限定的查询发出去,也不会显示范围外的行。definitionId:调用方画的是哪份定义的视图;别的定义的实例在运行前就被拒(view.open.other-definition)。tab、filters、held:只对看板有意义——打开在哪个页签、筛选初值、宿主固定住的筛选。
export interface OpenOptions {
definitionId?: string;
filters?: DashboardFilters | null;
held?: HeldFilters | null;
scopeFilter?: FilterTree | null;
tab?: string | null;
}export interface CreateInput<C extends ViewConfig> {
config: C;
scope: ViewAudience;
scopeFilter?: FilterTree | null;
title: string;
}CreateInput.scope 只能是受众(personal 或 shared):用户为受众新建,只有定义能声明系统视图。
ViewListing
export interface ViewListing {
failed: Issue | null;
items: ViewInstanceSummary[];
}完整可运行版本
- Storybook 的接入导览用同样几步接一个订单对象,页底就是它在运行。
- 源文件:
ordersEngine.ts、wowSource.ts;引擎本身在src/runtime/viewEngine.ts。