React Hooks
/react 是默认界面之下的那一层:同样的控制器,不带任何外观。/ui 的组件消费的正是它,所以用这些 Hooks 自己搭界面,行为与默认界面一致——同一套准入、同一条写入路径、同样的恢复动作。只想换掉一个单元格或加一个按钮时用 /ui 的插槽就够了;整块界面要自己画时才来这一层。
它们共同的约定:
- Hooks 接收引擎,从不接收选项。
useViewEngine是唯一的例外,它为组件的生命周期建一个引擎,卸载时释放;需要更长生命期的应用自己建引擎,再交给其余 Hooks。 - 命令 resolve 而不 reject:
useSaveCommands与useViewManager把结局放进状态,点击处理器不需要 try/catch,没解决的写入一直可见,直到用户重试、覆盖或放弃。 - 加载状态是推导的:只要手上的回答属于另一次请求,这一次就还在进行,所以 effect 只在回答到达时写状态;在它之后才到的回答被丢弃,而不是套到没人在看的视图上。
import { systemInstanceId } from '@ahoo-wang/wow-view-engine';
import { useOpenView, useRecordTable, useViewRuntime } from '@ahoo-wang/wow-view-engine/react';
export function ToShipCount() {
// 定义 orders 在代码里声明的系统视图 to-ship:system:orders:to-ship。
const { runtime, loading, error } = useOpenView(engine, systemInstanceId('orders', 'to-ship'));
useViewRuntime(runtime);
const table = useRecordTable(runtime?.kind === 'record' ? runtime : null);
if (error) return <p role="alert">{error.code}</p>;
if (loading || !table.hasResult) return <p>…</p>;
return <p>待发货 {table.rows.length} 张</p>;
}相关指南:视图引擎的核心概念(这些 Hook 打开与订阅的视图与运行时)、声明式操作(只换一个单元格或按钮时的插槽)。
打开与订阅
| Hook | 作用 |
|---|---|
useViewEngine(options) | 为组件的生命周期建一个引擎,卸载时释放。选项只读一次:定义集合与存储不是渲染期的值 |
useOpenView(engine, instanceId, scopeFilter?, opening?) | 打开一个实例并拥有它产生的运行时:换 id 或卸载时释放前一个。被引擎释放的运行时(实例被删)不会交出,id 会再开一次。scopeFilter 从第一次查询起生效,开时读一次之后跟随,每次渲染传新对象也不会重开;定义拒绝的条件不生效,scopeIssues 说明哪个没生效。opening 给看板说打开在哪个页签、筛选是什么 |
useViewRuntime(runtime) | 订阅一个运行时,返回快照。运行时先提交状态再通知,没变化时返回同一个对象——正是 useSyncExternalStore 对存储的要求 |
useOpenView 的 retry() 再开同一个 id:给可能会过去的失败(存储不可达,或服务端答了自己的错误)一条回头路。
export declare function useViewEngine(options: ViewEngineOptions): ViewEngine;
export declare function useOpenView(engine: ViewEngine, instanceId: string | null, scopeFilter?: FilterTree | null, opening?: (instanceId: string) => Omit<OpenOptions, 'scopeFilter'> | undefined): OpenViewState;
export declare function useViewRuntime<R extends ViewRuntimeStore<unknown>>(runtime: R | null): SnapshotOf<R> | null;
export interface OpenViewState {
error: Issue | null;
loading: boolean;
retry(): void;
runtime: AnyViewRuntime | null;
scopeIssues: Issue[];
}视图列表与管理
useViewList 给出一份定义的列表、偏好与权限。它们各自加载、互不阻塞:列表失败不影响已打开的视图,偏好失败只是退回服务端顺序。options.kinds 在排序与解析缺省之前把列表收窄到这些种类——工作台只画它有部件的种类,否则侧栏会提供画不出来的视图。
all 是收窄前的同一批摘要:调整顺序存的是整份定义的一个顺序,收窄的工作台只提交它看得见的会把其他 id 从 preferences.order 里丢掉,所以写顺序的调用方从 all 读,只在其中移动它看得见的两个 id。
useViewManager 管列表上的视图而不是打开着的那个:改名、换受众、删除、调整顺序、选缺省,以及没有运行时拥有的写入的恢复动作。落地的写入会重新加载列表,因为变的就是列表。
export declare function useViewList(engine: ViewEngine, definitionId: string, options?: ViewListOptions): ViewListState;
export declare function useViewManager(engine: ViewEngine, definitionId: string, list: ViewListState): ViewManagerController;
export interface ViewListState {
all: ViewInstanceSummary[];
defaultInstanceId: string | null;
error: Issue | null;
items: ViewInstanceSummary[];
loading: boolean;
permissions: ViewPermissions;
preferences: ViewPreferences | null;
preferencesError: Issue | null;
preferencesSettled: boolean;
reload(options?: ViewListReloadOptions): void;
}工作台外壳
useWorkbench 是一个工作台的外壳,不带任何外观:视图怎样被找到、打开、离开、保存。种类是参数——记录、分析与看板的不同在于它们从 runtime 渲染的编辑器与结果,而不在这些流程。唯一读种类的是 drill:打开分析结果一个分组背后的记录(它要求打开的是分析视图、且 record 在这个工作台列出的种类里)。
useLeaveGuard 是一个打开的视图与下一个之间的那次确认。打开另一个视图会释放这个视图的运行时,而未保存的草稿只活在运行时里——所以离开就是删除工作,问一次;没有东西可丢的视图从不问。页面内切换与离开页面是同一条规则:侧栏、切换器和推过来的 instanceId 走 request,标签页自己的关闭走 beforeunload(guardUnload,缺省开;只有拥有整个页面的宿主才该关掉它)。嵌入从不调用它。
export declare function useWorkbench(engine: ViewEngine, definitionId: string, options: WorkbenchOptions): WorkbenchController;
export declare function useLeaveGuard(state: LeaveGuardState | null, input?: LeaveGuardOptions): LeaveGuard;
export interface WorkbenchOptions {
guardUnload?: boolean;
handOver?: ViewHandOver | null;
instanceId?: string | null;
kinds: readonly ViewKind[];
newView?: NewViewOptions;
onDrilldown?(target: DrillTarget): void;
onInstanceChange?(id: string | null): void;
onNavigate?(to: ViewNavigation): void;
opening?(instanceId: string): DashboardOpening | undefined;
}编辑与结果
| Hook | 作用 |
|---|---|
useFilterEditor(runtime) | 按路径编辑草稿的条件树。它自己不持有状态:每个动作都是对运行时的一次 edit,所以同一视图上的两个编辑器意见一致,撤销就是不提交。看板上没有可编辑的树,编辑什么也不做 |
useRecordTable(runtime) | 表格渲染的记录视图,看不到任何第三方类型。行和列来自最后一次成功的结果而不是草稿,所以改列立即生效,没有别的在等时排序也立即生效(toggleSort),改条件要等提交 |
useAnalysisEditor(runtime) | 分析视图的草稿编辑:分组、指标、条件、排序与条数,以及它们各自能选什么 |
useAnalysisResult(runtime, analysis, workbench) | 宿主画出的分析结果:哪些行、上面画哪张图、选图器提供什么、按一个分组接下来能做什么。行是运行过的配置,怎么看是草稿:版式和图表是呈现,换图不重跑 |
useDashboard(runtime) | 看板作为面板网格。它报告已应用的面板而不是草稿的,因为面板是正在运行的查询;几何是例外——拖动一个面板只应用那次移动 |
export declare function useFilterEditor(runtime: ViewRuntime | null): FilterEditorController;
export declare function useRecordTable(runtime: RecordViewRuntime | null): RecordTableController;
export declare function useAnalysisEditor(runtime: ViewRuntime | null): AnalysisEditorController;
export declare function useAnalysisResult(runtime: ViewRuntime<AnalysisViewConfig> | null, analysis: AnalysisEditorController, workbench: Pick<WorkbenchController, 'state' | 'canDrill' | 'drill' | 'follow'>): AnalysisResultController;
export declare function useDashboard(runtime: DashboardRuntime | null): DashboardController;写入命令
useSaveCommands 是一个打开视图的写入命令,连同决定哪些按钮可用的权限。每个命令都 resolve:结局落进 state。
export declare function useSaveCommands(engine: ViewEngine, runtime: ViewRuntime | null): SaveCommands;声明的操作
useRecordActions 把声明的操作(actions())放到宿主自己画的表格上:哪些行提供哪个、可不可用、选择怎样分组,以及执行。
| 选项 | 作用 |
|---|---|
table | 操作所在的表格(useRecordTable 的控制器满足它) |
actions | 声明的操作;不传只剩执行器 |
concurrency | 同时在途的记录数,缺省几条 |
refresh | 命令之后「重读视图」在这里是什么意思:缺省是表格自己的;看板的记录面板重读整块看板 |
also | 表格当前页以外显示的记录——由链接打开的记录详情——它们的操作也要画,可用性也会自己变化 |
onError | 命令在某条记录上抛出的东西,原样告诉宿主——不包括操作自己的拒绝,也不包括中止 |
RecordActionSlots(global、bulk、row 三个渲染函数)是插槽:画在声明的操作之后的宿主标记。
export declare function useRecordActions(input: RecordActionsOptions): RecordActionsController;
export interface RecordActionsOptions {
actions?: RecordActions;
also?: readonly RecordRow[];
concurrency?: number;
onError?(error: unknown, context: ActionFailureContext): void;
refresh?(): void;
table: RecordActionTable;
}
export interface RecordActionSlots {
bulk?(context: RecordBulkActionContext): import('react').ReactNode;
global?(context: RecordGlobalActionContext): import('react').ReactNode;
row?(context: RecordRowActionContext): import('react').ReactNode;
}完整可运行版本
- 用这些 Hooks 搭的一个工作台:引擎源码里的
examples/PlainRecordWorkbench.tsx。 /ui的组件就是这些 Hooks 的消费者:src/ui/DataWorkbench.tsx;Hooks 本身在src/react/。- Storybook:记录工作台。