阅读之前
你应该:
- 了解DSL相关内容。母版-布局-DSL 渲染基础(v4)
- 了解SPI机制相关内容。组件SPI机制(v4)
- 了解组件相关内容。
组件数据交互概述
数据结构设计
数据结构分为三大类,列表(List)
、对象(Object)
以及弹出层(Popup)
。
- 列表(List):用于多条数据的展示,主要包括
搜索(用户端)
、自定义条件(产品端)
、排序
、分页
、数据选中
、数据提交
、数据校验
功能。 - 对象(Object):用于单条数据的展示,主要包括
数据提交
、数据校验
功能。 - 弹出层(Popup):用于在一块独立的空间展示对应类型的数据。
数据结构与对于的内置视图类型:
- 列表(List):
表格视图(TABLE)
、画廊视图(GALLERY)
- 对象(Object):
表单视图(FORM)
、详情视图(DETAIL)
数据交互设计原则
组件与组件之间的结构关系是独立的,组件与组件之间的数据是关联的。因此,数据交互整体采用“作用域隔离(View),行为互通(CallChaining),数据互通(ActiveRecords)”这样的基本原则进行设计。实现时,围绕不同视图类型定义了一类数据结构所需的基本属性。
在弹出层进行设计时,使用Submetadata
的方式,将包括弹出层在内的所有组件包含在内,以形成新的作用域。
通用属性及方法
属性
- rootData:根数据源
- dataSource:当前数据源
- activeRecords:当前激活数据源
为了在实现时包含所有数据结构,我们统一采用ActiveRecord[]
类型为所有数据源的类型,这些数据源在不同类型的视图中表示不同的含义:
- 列表(List):
dataSource
为列表当前数据源,activeRecords
为列表中被选中的数据。特别的,showDataSource
为当前展示的数据源,它是dataSource
经过搜索、排序、分页等处理后的数据源,也是我们在组件中真正使用的数据源。 - 对象(Object):
daraSource
和activeRecords
总是完全一致的,且长度永远为1
。因此我们有时也在组件中定义formData
属性,并提供默认实现:this.activeRecords?.[0] || {}
。
方法
- reloadDataSource:重载当前数据源
- reloadActiveRecords:重载当前激活数据源
由于底层实现并不能正确判断当前使用的数据类型,因此我们无法采用统一标准的数据源修改方法,这时候需要开发者们自行判断。
重载列表数据源
cosnt dataSource: ActiveRecord[] = 新的数据源
// 重载数据源
this.reloadDataSource(dataSource);
// 重置选中行
this.reloadActiveRecords([]);
重载表单数据源
cosnt dataSource: ActiveRecord[] = 新的数据源(数组中有且仅有一项)
// 重载数据源
this.reloadDataSource(dataSource);
// 此处必须保持相同引用
this.reloadActiveRecords(dataSource);
内置CallChaining(链式调用)
在自动化渲染过程中,我们通常无法明确知道当前组件
与子组件
交互的具体情况,甚至我们在定义当前组件
时,并不需要关心(某些情况下可能无法关心)子组件
的具体情况。这也决定了我们无法在任何一个组件中完整定义所需的一切功能。
为了保证组件行为的一致性,我们需要某些行为在各个组件的实现需要做到组件自治
。以表格视图
为例:当view标签
在挂载时,我们无法确定应该怎样正确的加载数据,因此,我们需要交给一个具体的element标签
来完成这一功能。当element标签
对应的组件发生变化时,只需按照既定的重载
方式将数据源提交给view标签
即可。
除了保证组件行为的一致性外,我们不能完全的信任
第三方框架对组件生命周期的处理顺序。因此,我们还需要对组件行为进行进一步的有序
处理。以表格视图
为例:我们希望搜索视图(SEARCH)
的处理总是在加载数据
前就处理完成的,这样将可以保证我们加载数据
可以正确处理搜索条件,而这一特性并不随着模板
结构的变化而发生变化。
平台内置的CallChaining
- mountedCallChaining:挂载时钩子;
- refreshCallChaining:刷新时钩子;
- submitCallChaining:提交时钩子;
- validatorCallChaining:验证时钩子;
优先级常量
- VIEW_WIDGET_PRIORITY(
0
):视图组件优先级 - FETCH_DATA_WIDGET_PRIORITY(
100
):数据提供者组件优先级 - SUBVIEW_WIDGET_PRIORITY(
200
):子视图组件优先级
未设置优先级的hook将最后执行,在通常情况下,你无需关心优先级的问题。
注意事项
- CallChaining通常不需要手动初始化,仅需通过
inject
方式获取即可。 - CallChaining的hook/unhook方法需要在组件生命周期的
mounted
/unmounted
分别执行,如无特殊情况,一般通过this.path
作为挂载钩子的唯一键。
在字段组件
中使用mountedCallChaing
@Widget.Reactive()
@Widget.Inject()
protected mountedCallChaining: CallChaining | undefined;
protected mountedProcess() {
// 挂载时处理
}
protected mounted() {
super.mounted();
this.mountedCallChaining?.hook(this.path, () => {
this.mountedProcess();
});
}
protected unmounted() {
super.unmounted();
this.mountedCallChaining?.unhook(this.path);
}
字段组件
的mountedCallChaing
并不是必须的,因此我们未进行内置处理。
一般的,当视图数据被加载完成时,字段组件
的formData
和value
等属性,将通过响应式
自动获取对应的值,因此在大多数情况下是不需要使用这一特性的。
当我们需要对字段获取的值做进一步初始化处理时,我们将需要使用这一特性。例如TreeSelect
组件,必须在初始化时填充树下拉所需的结构化数据,这样才能正确展示对应的值。
当字段组件
的mounted
方法被执行时,我们还未执行视图数据加载,因此,在我们无法在mounted
方法中操作formData
和value
等属性,只有在mountedCallChaining
被view标签
执行时,按照执行顺序,此时字段的mountedChaining
将在视图数据被加载完成后执行。
数据源持有者和数据源提供者
在设计上,我们通常将view标签
设计为数据源持有者
,将element标签
设计为数据源提供者
。
原则上,在一个视图中有且仅有一个数据源提供者。
即:当一个element标签
的实现组件通过reloadDataSource方法
向view标签
设置数据源,我们就称该实现组件为当前view标签
的数据源提供者
,view标签
为数据源持有者
。
provider/inject
阅读该章节需要理解vue的依赖注入原理
在实现上,我们通过provider/inject
机制将上述通用属性/方法进行交替处理,就可以实现根据模板定义的结构进行隔离和共享功能。
例如dataSource
属性的实现:
/**
* 上级数据源
* @protected
*/
@Widget.Reactive()
@Widget.Inject('dataSource')
protected parentDataSource: ActiveRecord[] | undefined;
/**
* 当前数据源
* @protected
*/
@Widget.Reactive()
private currentDataSource: ActiveRecord[] | null | undefined;
/**
* 提供给下级的数据源
* @protected
*/
@Widget.Reactive()
@Widget.Provide()
public get dataSource(): ActiveRecord[] | undefined {
return this.currentDataSource || this.parentDataSource;
}
不足的是,由于provider/inject机制特性决定,通过provider提供的属性和方法,在某些情况下可能会进行穿透
,导致组件通过inject获取的属性和方法并非是我们所期望的那样,因此,我们仍然需要进行一些特殊的处理,才能正确的处理子视图
的数据交互,这一点在对象(Object)
类型的视图中会详细介绍。
运行时上下文(metadataHandle/rootHandle)
在之前的文章中,我们知道前端的字段/动作组件
渲染和后端元数据之间是密不可分的。
在数据交互方面,后端元数据对于字段类型
的定义,将决定从API接口中获取的字段、数据类型和格式
,以及通过API接口提交数据到后端时的字段、数据类型和格式
。
数据类型和格式
可以通过field标签
的data
属性,获取到经过后端编译的相关字段元数据。
那么,我们该如何决定,当数据提供者
向后端发起请求时,应该获取哪些字段呢?
因此,我们设计了RuntimeContext
机制,并通过metadataHandle/rootHandle
机制,在任何一个组件都可以通过view标签
正确获取已经实现隔离的运行时上下文
机制。
以表单视图为例,我们来看这样一个经过合并后的完整视图模板:
(以下模板所展示的并非实际的运行时的结果,而是为了方便描述所提供的完整视图模板)
<view type="FORM">
<element widget="actions">
<action name="create" invisible="activeRecord.id" />
<action name="update" invisible="!activeRecord.id" />
</element>
<element widget="fields">
<field data="id" invisible="true" />
<field data="code" />
<field data="name" />
<field data="relationOne" widget="Form">
<view type="FORM">
<element widget="form">
<field data="id" invisible="true" />
<field data="code" />
<field data="name" />
</element>
</view>
</field>
<field data="relationMany" widget="Table">
<view type="TABLE">
<element widget="actionBar">
<action name="openCreateDialog">
<view type="FORM">
<element widget="fields">
<field data="id" invisible="true" />
<field data="code" />
<field data="name" />
</element>
</view>
</action>
</element>
<element widget="table">
<field data="id" invisible="true" />
<field data="code" />
<field data="name" />
<element widget="rowActions">
<action name="openEditDialog">
<view type="FORM">
<element widget="fields">
<field data="id" invisible="true" />
<field data="code" />
<field data="name" />
</element>
</view>
</action>
</element>
</element>
</view>
</field>
</element>
</view>
其中:
- relationOne字段:代表
M2O
字段,它使用Form
组件将字段展开为子表单视图
。(O2O
类型基本一致) - relationMany字段:代表
O2M
字段,它使用Table
组件将字段展开为子表格视图
。(M2M
类型基本一致)- openCreateDialog:代表打开一个
创建
弹窗,它是一个上下文类型为上下文无关
的跳转动作。 - openEditDialog:代表打开一个
编辑
弹窗,它是一个上下文类型为单行
的跳转动作。
- openCreateDialog:代表打开一个
PS:这里我们关注的是组件之间的数据交互,至于动作
相关内容,我们会在其他文章中做进一步介绍。在这里我们只需要了解,当按钮的上下文类型为上下文无关
时,该按钮将不会携带任何数据到下一个视图中;当按钮的上下文类型为单行
时,该按钮将携带activeRecords[0]
的数据到下一个视图中。
通过这个视图模板,我们将构建出如下图所示的RuntimeContext
结构:
由此可见,我们的metadataHandle
可能通过页面入口(ViewAction)
提供,也可能通过字段(field标签)
提供,还可能通过动作(action标签)
提供。无一例外的是,所有的view标签
都将生成一个与之对应的rootHandle
。
在编码过程中,我们可以通过this.metadataRuntimeContext
和this.rootRuntimeContext
获取到对应的运行时上下文。前端运行时上下文API文档
列表(List)数据交互
对于列表(List)
数据结构来说,我们目前提供了两种具体的视图:表格视图(TABLE)
、画廊视图(GALLERY)
,这两种视图分别对应不同的数据展示形态。
- 表格视图(TABLE):字段代表表格中的
列(Column)
,每一列的数据使用相同方式展示,无法使用布局特性。 - 画廊视图(GALLERY):使用
list
-item
形式进行渲染,每一个item
可以通过DSL描述其布局特性。
特别的是,搜索视图(SEARCH)
是对列表(List)
数据结构提供搜索(用户端)
相关功能的子视图
,其独立存在于页面中是没有意义的。
默认表格视图(TABLE)
<view type="TABLE">
<pack widget="group">
<view type="SEARCH">
<element widget="search" slot="search" />
</view>
</pack>
<pack widget="group" slot="tableGroup">
<element widget="actionBar" slot="actionBar">
<xslot name="actions" />
</element>
<element widget="table" slot="table">
<element widget="expandColumn" slot="expandRow" />
<xslot name="fields" />
<element widget="rowActions" slot="rowActions" />
</element>
</pack>
</view>
在之前的文章中,我们了解了布局和标签的基本概念,为了能更好的说明数据交互相关的特征,我们将上述视图进行简化,仅留下与数据交互相关的标签。
<view type="TABLE">
<view type="SEARCH">
<element widget="search" slot="search" />
</view>
<element widget="actionBar" slot="actionBar">
<xslot name="actions" />
</element>
<element widget="table" slot="table">
<xslot name="fields" />
<element widget="rowActions" slot="rowActions" />
</element>
</view>
在这个视图中,我们可以获得如下信息:
- 主视图为
表格(TABLE)
视图。 - 包含一个
搜索(SEARCH)
子视图。 - 包含两个主要的
element组件
:widget="actionBar"
和widget="table"
。 - 在
widget="table"
中内置了一个element组件
:widget="rowActions"
。
在我们内置的组件实现中,widget="table"
这个组件通过调用后端接口,为整个视图提供数据源。
组件挂载时序图
在上图中,我们可以看到,mountedCallChaining
是在所有组件全部渲染并挂载完成后,通过最上层的view标签
进行调用的,并且按照一定的优先级顺序执行了每个组件实现的挂载钩子函数
。
组件数据源加载
当widget="search"
组件执行了挂载钩子函数
时,将从url参数
中获取所需的searchBody
和searchCondition
属性,并通过flushSearchParameters
方法将这两个参数提交到表格视图(TABLE)
。
当widget="table"
组件执行了挂载钩子函数
时,将执行加载数据相关功能,并通过reloadDataSource
和reloadActiveRecords
方法对数据源进行向上提交。
用户行为触发的数据交互
这里列举了一些列表(List)
常用的数据交互时序图,用于帮助大家对数据交互的整体逻辑可以有更清晰的理解。
点击搜索
排序/分页
表格勾选(table checkbox/radio)
对象(Object)数据交互
对于对象(Object)
数据结构来说,我们目前提供了两种具体的视图:表单视图(FORM)
、详情视图(DETAIL)
,这两种视图分别对应不同的数据展示形态。
- 表单视图(FORM):主要用于数据的填写和提交;可以通过DSL描述其布局特性。
- 详情视图(DETAIL):主要用于数据的展示,所有字段强制为只读态(与表单的只读态有所不同);可以通过DSL描述其布局特性。
特别的是,表单视图(FORM)
和详情视图(DETAIL)
在对子视图
的处理上并不完全相同。主要体现在数据提交类型(submitType)
和关联关系更新类型(relationUpdateType)
这两个属性的处理上。
数据提交类型(submitType)
/**
* 数据提交类型
*/
export enum SubmitType {
/**
* <h3>默认</h3>
* <p>
* 基础字段使用全量提交,关联关系字段使用关联关系提交
* </p>
*/
default = 'default',
/**
* 不提交
*/
none = 'none',
/**
* 关联关系字段关系提交(仅对关联关系字段有效)
*/
relation = 'relation',
/**
* 关联关系字段增量提交(仅对关联关系字段有效)
*/
increment = 'increment',
/**
* 全量提交
*/
all = 'all'
}
关联关系更新类型(relationUpdateType)
/**
* 关联关系数据更新类型
*/
export enum RelationUpdateType {
/**
* <h3>默认</h3>
* <p>
* 默认根据数据提交类型进行自动识别
* </p>
*/
default = 'default',
/**
* 使用特定接口进行差量更新(关联关系字段强制使用增量提交)
*/
diff = 'diff',
/**
* 使用batch接口进行差量更新(关联关系字段强制使用增量提交)
*/
batch = 'batch',
/**
* 全量更新
*/
all = 'all'
}
下面分别列举了一些字段组件
在不同视图中的默认功能。
表单视图(FORM)
组件 (widget) | 一对一(O2O) | 多对一(M2O) | 一对多(O2M) | 多对多(M2M) |
---|---|---|---|---|
Table | / | / | 前端搜索、分页、排序 submitType="all" relationUpdateType="all" |
前端搜索、分页、排序 submitType="relation" relationUpdateType="all" |
Form | submitType="all" relationUpdateType="all" |
submitType="all" relationUpdateType="all" |
/ | / |
Select | submitType="relation" relationUpdateType="all" |
submitType="relation" relationUpdateType="all" |
submitType="relation" relationUpdateType="all" |
submitType="relation" relationUpdateType="all" |
详情视图(DETAIL)
组件 (widget) | 一对一(O2O) | 多对一(M2O) | 一对多(O2M) | 多对多(M2M) |
---|---|---|---|---|
Table | / | / | 后端搜索、分页、排序 不支持数据提交 |
后端搜索、分页、排序 不支持数据提交 |
Form | 仅用于查看 submitType="relation" relationUpdateType="all" |
仅用于查看 submitType="relation" relationUpdateType="all" |
/ | / |
Select | 仅用于查看 submitType="relation" relationUpdateType="all" |
仅用于查看 submitType="relation" relationUpdateType="all" |
仅用于查看 submitType="relation" relationUpdateType="all" |
仅用于查看 submitType="relation" relationUpdateType="all" |
默认表单视图(FORM)
<view type="FORM">
<element widget="actionBar" slot="actionBar">
<xslot name="actions" />
</element>
<element widget="form" slot="form">
<xslot name="fields" />
</element>
</view>
在这个视图中,我们可以获得如下信息:
- 主视图为
表单(FORM)
视图。 - 包含两个主要的
element组件
:widget="actionBar"
和widget="form"
。
在我们内置的组件实现中,widget="form"
这个组件通过调用后端接口,为整个视图提供数据源。
组件挂载时序图
组件数据源加载
用户行为触发的数据交互
这里列举了一些对象(Object)
常用的数据交互时序图,用于帮助大家对数据交互的整体逻辑可以有更清晰的理解。
点击提交动作(ServerAction)
(创建/编辑)
Oinone社区 作者:oinone原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/26.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验