阅读之前
你应该:
- 了解DSL相关内容。母版-布局-DSL 渲染基础(v4)
- 了解SPI机制相关内容。组件SPI机制(v4.3.0)
- 了解组件相关内容。
为什么需要手动渲染
在自定义组件之自动渲染(组件插槽的使用)(v4)文章中,我们介绍了带有具名插槽的组件可以使用DSL模板进行自动化渲染,并且可以用相对简单的方式与元数据进行结合。
虽然自动化渲染在实现基本业务逻辑的情况下,有着良好的表现,但自动化渲染方式也有着不可避免的局限性。
比如:当需要多个视图在同一个位置进行切换
。
在我们的平台中,界面设计器
的设计页面,在任何一个组件
在选中后,需要渲染对应的右侧属性面板
。每个面板的视图
信息是保存在对应的元件
中的。根据元件
的不同,找到对应的视图
进行渲染。在单个视图中使用自动化渲染是无法处理这一问题的,我们需要一种可以局部渲染指定视图
的方式,来解决这一问题。
获取一个视图
使用ViewCache获取视图
export class ViewCache {
/**
* 通过模型编码和名称获取视图
* @param model 模型编码
* @param name 名称
* @param force 强制查询
* @return 运行时视图
*/
public static async get(model: string, name: string, force = false): Promise<RuntimeView | undefined>
/**
* 通过模型编码、自定义名称和模板获取编译后的视图(此视图非完整视图,仅用于自定义渲染使用)
* @param model 模型编码
* @param name 名称(用作缓存key)
* @param template 视图模板
* @param force 强制查询
* @return 运行时视图
*/
public static async compile(
model: string,
name: string,
template: string,
force = false
): Promise<RuntimeView | undefined>
}
- ViewCache#get:用于
服务端
定义视图,客户端
直接获取完整视图信息。 - ViewCache#compile:用于
客户端
定义视图,通过服务端
编译填充元数据相关信息,但不包含视图其他信息。
自定义一个带有具名插槽的组件,并提供切换视图的相关按钮
以下是一个自定义组件的完整示例,其使用ViewCache#compile
方法获取视图。
view.ts
const template1 = `<view>
<field data="id" invisible="true" />
<field data="code" label="编码" />
<field data="name" label="名称" />
</view>`;
const template2 = `<view>
<field data="id" invisible="true" />
<field data="name" label="名称" />
<field data="code" label="编码" />
</view>`;
export const templates = {
template1,
template2
};
ManualDemoWidget.ts
import {
BaseElementWidget,
createRuntimeContextForWidget,
FormWidget,
RuntimeView,
SPI,
ViewCache,
ViewType,
Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { templates } from './view';
@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
private formWidget: FormWidget | undefined;
public initialize(props) {
super.initialize(props);
this.setComponent(ManualDemo);
return this;
}
@Widget.Method()
public async resetTemplate(key: string): Promise<void> {
// 获取运行时视图
const view = await this.fetchViewByCompile(key);
if (!view) {
console.error('Invalid view');
return;
}
// 销毁旧组件
this.formWidget?.dispose();
this.formWidget = undefined;
// 创建运行时上下文
const runtimeContext = createRuntimeContextForWidget(view);
const runtimeContextHandle = runtimeContext.handle;
// 获取初始化数据
const formData = await runtimeContext.getInitialValue();
// 提交数据源到当前视图
this.reloadDataSource(formData);
this.reloadActiveRecords(formData);
// 创建所需组件
this.formWidget = this.createWidget(FormWidget, 'formWidget', {
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
template: runtimeContext.viewTemplate,
inline: true
});
}
public async fetchViewByCompile(key: string): Promise<RuntimeView | undefined> {
const template = templates[key];
if (!template) {
console.error('error template', key);
return;
}
// 模型编码
const model = ${model};
// 通过编译获取视图(非完整视图数据)
const view = await ViewCache.compile(model, key, template);
if (!view) {
return;
}
// 补充视图类型
view.type = ViewType.Form;
return view;
}
protected mounted() {
// 挂载时初始化指定视图
this.resetTemplate('template1');
}
}
ManualDemo.vue
<template>
<div class="manual-demo-wrapper" v-show="!invisible">
<oio-button @click="() => switchTemplate('template1')">切换模版1</oio-button>
<oio-button @click="() => switchTemplate('template2')">切换模版2</oio-button>
<div class="manual-demo-widget">
<slot name="formWidget" />
</div>
</div>
</template>
<script lang="ts">
import { OioButton } from '@kunlun/vue-ui-antd';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ManualDemo',
components: {
OioButton
},
props: {
invisible: {
type: Boolean,
default: undefined
},
resetTemplate: {
type: Function
}
},
setup(props) {
const switchTemplate = (key: string) => {
props.resetTemplate?.(key);
};
return {
switchTemplate
};
}
});
</script>
视图DSL
<view type="FORM">
<template slot="actionBar">
<action name="create" label="创建" />
</template>
<template slot="form" widget="ManualDemo" />
</view>
在上述示例中,我们在view.ts
定义了两个用于切换的视图模板,在视图DSL
指定widget="ManualDemo"
使用我们定义的组件,以此来实现我们的需求。
关键点详解
Class Component
与Vue组件
之间的交互
@Widget.Method()
public async resetTemplate(key: string): Promise<void> {
......
}
export default defineComponent({
props: {
resetTemplate: {
type: Function
}
},
setup(props) {
const switchTemplate = (key: string) => {
props.resetTemplate?.(key);
};
return {
switchTemplate
};
}
});
使用@Widget.Method()
装饰器,将resetTemplate
方法通过props
传递给Vue组件
,通过Vue组件
的用户行为(点击按钮)调用所需的执行逻辑。
在这个示例中,我们通过在setup
中定义switchTemplate
方法,来避免resetTemplate
方法可能为空
造成的一些异常。在最佳实践中,我们通常不建议开发者直接使用props
中定义的Function
类型。在大多数情况下,在Class Component
中定义的方法应该是尽可能抽象的,而不是为了某个Vue组件
提供的特殊方法。
手动渲染
常用步骤
- 获取运行时视图
- 销毁旧组件
- 创建运行时上下文
- 获取初始化数据
- 模板中定义的初始化数据
- 调用
服务端
接口获取
- 根据当前场景选择数据源持有者
- 提交数据源到当前视图
- 组件持有数据源
- 创建所需组件
一般情况下,我们仅需按照上述步骤就可以实现手动渲染
功能。
特别的,我们为了保证组件的生命周期完整,并且在运行时不会出现异常挂载/卸载的情况,自动渲染
和手动渲染
是不允许交叉使用的,并且根据组件所属的场景不同,手动渲染
比自动渲染
更关注数据交互
相关的内容。
有时可能会遇到数据交互
相关的意外
:
- 数据源无法共享导致的
动作
功能异常 - 数据源未正确提交到所需组件,可能被某个具有数据源的组件拦截或穿透提交到更上层组件。
获取运行时视图
public async fetchViewByCompile(key: string): Promise<RuntimeView | undefined> {
// 获取视图模板
const template = templates[key];
if (!template) {
console.error('error template', key);
return;
}
// 模型编码
const model = ${model};
// 通过编译获取视图(非完整视图数据)
const view = await ViewCache.compile(model, key, template);
if (!view) {
return;
}
// 补充视图类型
view.type = ViewType.Form;
return view;
}
在这个示例中,我们通过客户端
定义视图模板,传递给服务端
进行编译得到运行时视图,并手动补充了视图类型。
但通常情况下,我们采用的是通过服务端
定义视图模板,并根据上下文中提供的某些信息,动态获取这个视图的模型编码
和视图名称
,并使用ViewCache#get
方法获取的。
如下述方法定义:
public fetchView(model: string, name: string): Promise<RuntimeView | undefined> {
return ViewCache.get(model, name);
}
创建运行时上下文
const runtimeContext = createRuntimeContextForWidget(view);
const runtimeContextHandle = runtimeContext.handle;
createRuntimeContextForWidget
定义
/**
* 为组件创建运行时上下文,一般用于直接创建组件的场景
* 注:deep属性必须保持一致
* @param view 运行时视图
* @param options 创建可选项 默认值: mergeLayout = false, deep = true
* @return 组件运行时上下文
*/
export function createRuntimeContextForWidget(
view: RuntimeView,
options?: { mergeLayout?: boolean; deep?: boolean }
): RuntimeContext
此处的运行时上下文是针对某个具体组件的模板进行创建的,它与其他通过自动化渲染方式获取的上下文有所不同。
一般的,我们将传入的运行时视图
称为组件视图
,由此生成的运行时上下文称为组件运行时上下文
,将这个运行时上下文中获取的viewTemplate
称为组件模板
。
以template1
为例:
<view>
<field data="id" invisible="true" />
<field data="code" label="编码" />
<field data="name" label="名称" />
</view>
该模板的view标签
并非真实渲染了一个视图组件
,而是将其作为组件模板
的根标签
,其标签名称在运行时是没有任何含义的。
对于上述创建的FormWidget
组件来说,通过this.getDsl()
方法获取的DslDefinition
对象就是view标签
。
例如,我们将template1
改为:
<view layout="horizontal">
<field data="id" invisible="true" />
<field data="code" label="编码" />
<field data="name" label="名称" />
</view>
在FormWidget
组件中,通过this.getDsl().layout
将获取到horizontal
值。
创建所需组件
// 获取初始化数据
const formData = await runtimeContext.getInitialValue();
// 提交数据源到当前视图
this.reloadDataSource(formData);
this.reloadActiveRecords(formData);
// 创建所需组件
this.formWidget = this.createWidget(FormWidget, 'formWidget', {
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
template: runtimeContext.viewTemplate,
inline: true
});
在示例中,我们将数据源提交到当前视图用于和其他组件共享数据源,并创建了一个平台提供的FormWidget
组件。'formWidget'
为Vue组件
中定义的插槽名称。
创建任何组件时,metadataHandle
、rootHandle
、template
以及inline
属性是必须的。
metadataHandle
和rootHandle
:用于应用运行时上下文到当前组件。一般情况下,手动渲染
组件的这两个值始终是保持一致的。template
:用于指定组件使用的组件模板
。inline
:用于决定当前组件是否与浏览器的URL进行交互。
获取组件模板的初始化数据
- RuntimeContext#getDefaultValue:收集
组件模板
中field标签
定义的defaultValue
属性,并返回一个对象
。 - RuntimeContext#getInitialValue:在
RuntimeContext#getDefaultValue
方法的基础上,将field标签
定义的related
和compute
属性进行了首次计算,并返回一个对象
。(目前最新版本与RuntimeContext#getDefaultValue
一致,但我们建议开发者使用该方法)
通常情况下,我们也需要调用服务端
服务,来获取最终的初始化数据。在这里不做相关介绍。
根据当前场景选择数据源持有者
在数据交互
相关章节中,我们对数据源持有者
有详细介绍,在这里不再赘述。
在视图DSL
定义中,我们使用ManualDemo
组件的方式决定,我们在这里需要将数据源
提交到当前视图
,用于actionBar
组件中定义的动作
进行数据源共享。动作
执行时的所需的数据源,就是我们这里提交给当前视图的数据源。
一般情况下,手动渲染
组件的数据源不再通过组件的自动查询功能进行数据初始化,这又可能会导致数据交互
流程变得难以理解。因此我们建议手动渲染
组件的数据源总是在创建组件前就提前准备好的。
下面我们介绍如何让组件持有数据源,该方法并不适用于当前场景,但开发者有必要了解相关内容。
组件持有数据源
// 获取初始化数据
const formData = await runtimeContext.getInitialValue();
// 创建所需组件
this.formWidget = this.createWidget(FormWidget, 'formWidget', {
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
dataSource: formData,
activeRecords: formData,
template: runtimeContext.viewTemplate,
inline: true
});
与之前不同的是,我们将dataSource
和activeRecords
通过属性直接传递给组件实例即可。
在当前场景中,这样修改后的结果将导致页面中的按钮无法获取数据源,无法点击。
支持手动渲染
的组件
在平台提供的所有组件中,并非所有组件都支持手动渲染
。包括但不限于如下几个组件:
- TableWidget:表格组件
- FormWidget:表单组件
- DetailWidget:表单组件(详情)
如何让组件支持手动渲染
支持手动渲染
的组件与普通组件唯一的不同就是对metadataHandle
和rootHandle
的处理。
在Vue
框架下,我们提供了mixin混入
的方式,让开发者可以更简单的定义一个支持手动渲染
的组件。
组件注册、组件定义都无需改变,仅需在对应的Vue组件
中添加ManualWidget
混入即可。如下所示:
<script lang="ts">
import { ManualWidget } from '@kunlun/dependencies';
import { defineComponent } from 'vue';
export default defineComponent({
......
mixins: [ManualWidget],
......
});
</script>
Oinone社区 作者:数式-海波原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/32.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验