阅读之前
你应该:
- 了解DSL相关内容。母版-布局-DSL 渲染基础(v4)
组件SPI简介
不论是母版
、布局
还是DSL
,所有定义在模板中的标签都是通过组件SPI机制
获取到对应Class Component(ts)
并继续执行渲染逻辑。
基本概念:
- 标签:
xml
中的标签,json
中的dslNodeType属性
。 - Token组件:用于收集一组
Class Component(ts)
的基础组件。通常该基础组件包含了对应的一组基础能力(属性、函数等) - 维度(dsl属性):用于从
Token组件
收集的所有Class Component(ts)
组件中查找最佳匹配的参数。
组件SPI机制将通过指定维度
按照有权重的最长路径匹配
算法获取最佳匹配的组件。
组件注册到指定Token组件
以BaseFieldWidget
这个SPIToken组件为例,可以用如下方式,注册一个可以被field标签
处理的自定义组件:
(以下示例仅仅为了体现SPI注册的维度,而并非实际业务中使用的组件代码)
注册一个String类型
组件
维度:
- 视图类型:表单(FORM)
- 字段业务类型:String类型
说明:
- 该字段组件可以在
表单(FORM)
视图中使用 - 并且该字段的业务类型是
String类型
@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: ViewType.Form,
ttype: ModelFieldType.String
})
)
export class FormStringFieldWidget extends BaseFieldWidget {
......
}
注册一个多值
的String类型
组件
维度:
- 视图类型:表单(FORM)
- 字段业务类型:String类型
- 是否多值:是
说明:
- 该字段组件可以在
表单(FORM)
视图中使用 - 并且该字段的业务类型是
String类型
- 并且该字段为多值字段
@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: ViewType.Form,
ttype: ModelFieldType.String,
multi: true
})
)
export class FormStringMultiFieldWidget extends BaseFieldWidget {
......
}
注册一个String类型
的Hyperlinks
组件
维度:
- 视图类型:表单(FORM)
- 字段业务类型:String类型
- 组件名称:Hyperlinks
说明:
- 该字段组件仅可以在
表单(FORM)
视图中使用 - 并且该字段的业务类型是
String类型
- 并且组件名称必须指定为
Hyperlinks
@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: ViewType.Form,
ttype: ModelFieldType.String,
widget: 'Hyperlinks'
})
)
export class FormStringHyperlinksFieldWidget extends BaseFieldWidget {
......
}
当上述组件全部按顺序注册在BaseFieldWidget
这个SPIToken组件中时,将形成一个以BaseFieldWidget
为根节点的树:
树的构建
上述形成的组件树实际并非真实的存储结构,真实的存储结构是通过维度进行存储的,如下图所示:
(圆角矩形表示维度上的属性和值,矩形表示对应的组件)
有权重的最长路径匹配
同样以上述BaseFieldWidget
组件为例,该组件可用的维度有:
- viewType:ViewType[Enum]
- ttype:ModelFieldType[Enum]
- multi:[Boolean]
- widget:[String]
- model:[String]
- viewName:[String]
- name:[String]
当field标签
被渲染时,我们会组装一个描述当前获取维度的对象:
{
"viewType": "FORM",
"ttype": "STRING",
"multi": false,
"widget": "", // 在dsl中定义的任意值
"model": "", // 在dsl编译后自动填充
"viewName": "", // 当前视图名称
"name": "" // 字段的name属性,在dsl编译后自动填充
}
当我们需要使用FormStringHyperlinksFieldWidget
这个组件时,我们在dsl中会使用如下方式定义:
<view type="FORM" title="演示表单" name="演示模型form" model="demo.DemoModel">
<field data="name" widget="Hyperlinks" />
</view>
此时,我们虽然没有在dsl中定义维度中的其他信息,但在dsl返回到前端时,经过了后端编译填充了对应元数据相关属性,我们可以得到如下所示的对象:
{
"viewType": "FORM",
"ttype": "STRING",
"multi": false,
"widget": "Hyperlinks",
"model": "demo.DemoModel",
"viewName": "演示模型form",
"name": "name"
}
通过上述定义的对象,我们在存储结构中按照指定的维度顺序进行查找,就可以获取到我们需要的组件了。
查找过程简述:
- 匹配第一层viewType为FORM或包含FORM的节点
- 匹配第二层ttype为STRING或包含STRING的节点(此时,
FormStringFieldWidget
被插入到待返回队列
首位) - 匹配第三层multi为false的节点。(此时,没有任何节点匹配,继续匹配当前层)
- 匹配第三层widget为Hyperlinks的节点。(此时,
FormStringHyperlinksFieldWidget
被插入到待返回队列
首位) - 第四层为空,不再继续向下查找。
- 返回待返回队列首项。
特殊的默认组件
以BasePackWidget
为例,平台提供的DefaultGroupWidget
组件是这样注册的:
@SPI.ClassFactory(BasePackWidget.Token({}))
export class DefaultGroupWidget extends BasePackWidget {
......
}
该组件中不包含任何维度属性,我们无法将它添加到树中的任何一个节点,所以我们称这个组件在BasePackWidget
这个SPIToken中为默认组件
,任何SPIToken中的默认组件有且仅有一个。
因此,在dsl中,我们可以用如下方式直接使用这个默认组件:
<view type="FORM" title="演示表单" name="演示模型form" model="demo.DemoModel">
<pack>
<field data="name" widget="Hyperlinks" />
</pack>
</view>
通常情况下,我们希望dsl尽可能提供足够清楚的描述,因此,有时也可能看到这样的dsl模板:
<view type="FORM" title="演示表单" name="演示模型form" model="demo.DemoModel">
<pack widget="group">
<field data="name" widget="Hyperlinks" />
</pack>
</view>
由于平台并没有提供widget="group"
这个组件,因此,这两个dsl模板的最终执行结果是完全一致的。
精确匹配和模糊匹配
当我们希望一个组件可以在多个视图中共用时,我们通常使用这样的注册方式:
维度:
- 视图类型:表单(FORM)、搜索(SEARCH)
- 字段业务类型:String类型
说明:
- 该字段组件可以在
表单(FORM)
、搜索(SEARCH)
视图中使用 - 并且该字段的业务类型是
String类型
@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: [ViewType.Form, ViewType.Search],
ttype: ModelFieldType.String
})
)
export class FormStringFieldWidget extends BaseFieldWidget {
......
}
这样,我们就可以在多个视图中使用同一个组件。
母版中的标签
母版中的所有标签均使用MaskWidget
作为SPIToken组件。
(旧版使用ViewWidget
作为SPIToken组件,使用方式与下方描述完全不同,可略过组件注册相关内容)
维度:
- dslNodeType:
xml
中的标签 - widget:组件名称
标签组件概览
为了提供更好的灵活性,平台提供的所有标签组件,均支持class
和style
属性,在无法满足业务需求的情况下,可以使用这些特性进行处理。
标签 | 描述 |
---|---|
mask | 母版根标签 |
multi-tabs | 多选项卡 |
header | 顶部栏 |
container | 水平布局容器 |
sidebar | 侧边栏 |
content | 主内容区 |
block | 块(div) |
breadcrumb | 面包屑 |
widget | 母版通用组件 |
母版通用组件
母版通用组件全部使用widget
作为标签,使用widget
属性查找对应的组件。
例如:
<widget widget="app-switcher" />
标签 | 功能 |
---|---|
app-switcher | 应用切换组件 |
divider | 分割线 |
notification | 用户消息通知组件 |
language | 多语言切换组件 |
user | 用户信息展示组件 |
nav-menu | 导航菜单 |
main-view | 主视图组件;用于渲染布局和DSL等相关内容 |
默认母板
<mask>
<multi-tabs />
<header>
<widget widget="app-switcher" />
<block>
<widget widget="notification" />
<widget widget="divider" />
<widget widget="language" />
<widget widget="divider" />
<widget widget="user" />
</block>
</header>
<container>
<sidebar>
<widget widget="nav-menu" height="100%" />
</sidebar>
<content>
<breadcrumb />
<block width="100%">
<widget width="100%" widget="main-view" />
</block>
</content>
</container>
</mask>
母版标签注册
例如mask
标签的注册:
@SPI.ClassFactory(MaskWidget.Token({ dslNodeType: 'mask' }))
export class MaskRootWidget extends MaskWidget {
......
}
在母版中是这样使用的:
<mask>
......
</mask>
例如multi-tabs
标签的注册:
@SPI.ClassFactory(MaskWidget.Token({ dslNodeType: 'multi-tabs' }))
export class MultiTabsWidget extends MaskWidget {
......
}
在母版中是这样使用的:
<mask>
<multi-tabs />
......
</mask>
通用母版组件的注册
例如widget="main-view"
这样的组件注册:
@SPI.ClassFactory(MaskWidget.Token({ widget: 'main-view' }))
export class MainViewWidget extends MaskWidget {
......
}
在母版中是这样使用的:
<mask>
<widget widget="main-view" />
......
</mask>
替换母版中的平台内置组件
当使用与平台内置组件注册条件一致的SPIToken进行注册时,将实现内置组件的替换。
以多选项卡(multi-tabs)为例:
@SPI.ClassFactory(MaskWidget.Token({ dslNodeType: 'multi-tabs' }))
export class CustomMultiTabsWidget extends MaskWidget {
......
}
标签组件和通用母版组件的区别
使用标签组件
时,该组件将完全控制当前组件的渲染逻辑,与框架本身的渲染逻辑是完全一致的。
使用通用模板组件时,该组件将被默认包裹在一个特定div
标签下。
下面我们通过示例来了解一下。
先定义一个vue组件,在ts组件中通过不同的注册方式,将获得不同的渲染结果。
CustomMaskHelloWorld.vue
<template>
<div>hello world !</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'CustomMaskHelloWorld'
});
</script>
使用标签组件
CustomMaskHelloWorldWidget.ts
@SPI.ClassFactory(MaskWidget.Token({ dslNodeType: 'custom-mask-widget' }))
export class CustomMaskHelloWorldWidget extends MaskWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(CustomMaskHelloWorld);
return this;
}
}
<mask>
<custom-mask-widget />
......
</mask>
最终渲染结果
<div class="k-layout-mask">
<div>hello world !</div>
......
</div>
使用通用母版组件注册
CustomMaskHelloWorldWidget.ts
@SPI.ClassFactory(MaskWidget.Token({ widget: 'custom-mask-widget' }))
export class CustomMaskHelloWorldWidget extends MaskWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(CustomMaskHelloWorld);
return this;
}
}
<mask>
<widget widget="custom-mask-widget" />
......
</mask>
最终渲染结果
<div class="k-layout-mask">
<div class="k-layout-widget">
<div>hello world !</div>
</div>
......
</div>
布局中的标签
标签 | Token组件 | 维度(dsl属性) | 可选项 | 描述 |
---|---|---|---|---|
view | BaseView | type | ViewType[Enum] | 视图标签;主要用于元数据隔离; |
pack | BasePackWidget | viewType widget inline |
ViewType[Enum] [String] [Boolean] |
容器类组件标签 |
element | BaseElementWidget | viewType widget inline |
ViewType[Enum] [String] [Boolean] |
任意元素组件标签 |
默认布局
平台根据不同的视图类型内置了一些默认布局模板。参考文档
DSL中的标签
标签 | Token组件 | 维度(dsl属性) | 可选项 | 描述 |
---|---|---|---|---|
view | BaseView | type | ViewType[Enum] | 视图标签;主要用于元数据隔离; |
field | BaseFieldWidget | viewType ttype multi widget model viewName name |
ViewType[Enum] ModelFieldType[Enum] [Boolean] [String] [String] [String] [String] |
字段元数据标签 |
action | BaseActionWidget | viewType actionType target widget viewName model name |
ViewType[Enum] ActionType[Enum] ViewActionTarget[Enum] [String] [String] [String] [String] |
动作元数据标签 |
pack | BasePackWidget | viewType widget inline |
ViewType[Enum] [String] [Boolean] |
容器类组件标签 |
element | BaseElementWidget | viewType widget inline |
ViewType[Enum] [String] [Boolean] |
任意元素组件标签 |
在上表中,我们可以看到,pack组件
和element组件
的获取维度虽然是类似的,但我们依然将其进行了拆分。原因在于,当pack组件
中不包含任何元素或所有元素都隐藏时,我们希望pack组件
可以同时隐藏,但element组件
则无法确定是否需要这样的特性,因此element组件
默认没有进行这样的隐藏处理。
写在最后
在渲染母版
、布局
和DSL
时,组件SPI机制
使得动态组件可以按照设计好的维度
进行获取,使得动态渲染可以按照一定的规范进行二次改造。
为了使得动态渲染可以更加灵活,我们提供了自定义标签注册
、自定义创建SPIToken
等功能,开发者们完全可以根据自己的理解设计出一套全新的模板语法,以弥补我们在平台内置方面的不足。
HTML标签、Vue组件,都是按照标签
进行简单的获取组件,这在框架层面来说是完全足够的。但美中不足的是,如果全部使用标签形式来设计我们的模板语法,会导致模板语法难以阅读和理解。每个人都需要知道这个标签背后的实现逻辑才可能清楚的理解这段模板所描述的主要内容,再加上开发人员的独特理解风格,最终必然会导致对这段模板语法的解释只能由为数不多的开发人员理解。为了避免这些语法理解上的差异化,使用被设计好的维度
来保证每个人的理解是尽可能保持一致的。
例如一个“糟糕”的模板:
<view type="FORM" title="演示表单" name="演示模型form" model="demo.DemoModel">
<field data="id" invisible="true" />
<my-widget1>
<field data="name" />
</my-widget1>
<my-widget2>
<field data="isEnabled" />
</my-widget2>
</view>
这段模板很难推断my-widget1
和my-widget2
这两个组件所承担的主要功能。
但如果使用这样的模板进行描述:
<view type="FORM" title="演示表单" name="演示模型form" model="demo.DemoModel">
<field data="id" invisible="true" />
<pack widget="my-widget1">
<field data="name" />
</pack>
<pack widget="my-widget2">
<field data="isEnabled" />
</pack>
</view>
我们虽然同样无法确定这两个组件所承担的具体功能,但pack标签
的特性将告诉我们,这两个组件至少是一个容器类的组件,它本身所承担的是描述表单布局相关功能的组件。
不过,显而易见的是,虽然我们提供了一整套标准的模板语法来描述一个页面是如何呈现的,但仍然无法阻止开发人员在实现一个具体功能的组件时,一定要按照设计规范来编码。
因此,我们希望所有的开发者们,可以遵循这套设计规范来定义组件以及实现组件。
Oinone社区 作者:oinone原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/25.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验