概述
不论是母版
、布局
还是DSL
,我们统一使用XML
进行定义,可以更好的提供结构化
表述。
参考文档:
下面文档中未介绍到的
Mask母版
和Layout布局
,可以去数据库中base库的表base_layout_definition
和base_mask_definition
的template
字段查看
母版
确定了主题、非主内容分发区域所使用组件和主内容分发区域联动方式的页面配置。
母版内容分为主内容分发区域与非主内容分发区域。非主内容分发区域一般包含顶部栏、底部栏和侧边栏。侧边栏可以放置菜单,菜单与主内容分发区域内容进行联动。
默认母板
<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:母版根标签
- multi-tabs:多选项卡
- header:顶部栏
- container:容器
- sldebar:侧边栏
- nav-menu:导航菜单
- content:主内容
- breadcrumb:面包屑
- block:div块
- main-view:主视图;用于渲染布局和DSL等相关内容;
母版将整个页面的大体框架进行了描述,接下来将主要介绍布局和DSL是如何在main-view
中进行渲染的。关于自定义母版组件的相关内容 点击查看
布局
布局是将页面拆分成一个一个的小单元,按照从上到下、从左到右进行顺序排列
布局主要用于控制页面中元素的展示的相对位置,原则上不建议将元数据相关内容在布局中进行使用,可最大化布局的利用率。
默认表格视图(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:视图;用于定义当前视图类型,不同的视图类型会有不同的数据交互,以及渲染不同的组件。
- pack:容器类型相关组件。
- element:元素组件;包含各种各样的组件,根据组件实现有不同的作用。
- xslot:DSL插槽;用于将DSL中定义的模板分别插入到对应的槽中;
特别的,任何XML标签
上的slot
属性都具备DSL插槽
的全部能力。当学习完DSL
相关内容后,我们将会对DSL插槽
有比较清晰的理解。
PS:在下面的内容中,将使用该布局进行描述。
DSL
准备工作
为了方便描述DSL和元数据之间的关系,我们需要先定义一个简单模型,这个模型里面包含字段和动作。这些通常是服务端定义的。(对服务端不感兴趣的同学可以跳过代码部分)
DemoModel.java
@Model.model(DemoModel.MODEL_MODEL)
@Model(displayName = "演示模型", labelFields = {"name"})
public class DemoModel extends IdModel {
private static final long serialVersionUID = -7211802945795866154L;
public static final String MODEL_MODEL = "demo.DemoModel";
@Field(displayName = "名称")
private String name;
@Field(displayName = "是否启用")
private Boolean isEnabled;
}
DemoModelAction.java
@Model.model(DemoModel.MODEL_MODEL)
@UxRouteButton(
action = @UxAction(name = "redirectCreatePage", displayName = "创建", contextType = ActionContextTypeEnum.CONTEXT_FREE),
value = @UxRoute(model = DemoModel.MODEL_MODEL, viewName = "演示模型form"))
@UxRouteButton(
action = @UxAction(name = "redirectUpdatePage", displayName = "编辑", contextType = ActionContextTypeEnum.SINGLE),
value = @UxRoute(model = DemoModel.MODEL_MODEL, viewName = "演示模型form"))
public class DemoModelAction {
@Action(displayName = "启用")
public DemoModel enable(DemoModel data) {
data.setIsEnabled(true);
data.updateById();
return data;
}
@Action(displayName = "禁用")
public DemoModel disable(DemoModel data) {
data.setIsEnabled(false);
data.updateById();
return data;
}
}
上面的java代码定义了演示模型
的字段
和动作
:
- 字段
field
- id:ID
整数 Integer
- name:名称
字符串 String
- isEnabled:是否启用
布尔 Boolean
- id:ID
- 动作
action
- redirectCreatePage:创建;目标视图为"演示模型form"
跳转动作 ViewAction
- redirectUpdatePage:编辑;目标视图为"演示模型form"
跳转动作 ViewAction
- enable:启用
提交动作 ServerAction
- disable:禁用
提交动作 ServerAction
- redirectCreatePage:创建;目标视图为"演示模型form"
根据上面的元数据内容,我们可以定义一个如下所示的DSL模板,来使用所有的元数据将其渲染到页面中:
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
<template slot="search">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</template>
<template slot="actions">
<action name="redirectCreatePage" />
</template>
<template slot="fields">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</template>
<template slot="rowActions">
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</template>
</view>
该模板中包含的标签含义:
- view:视图;与布局模板中的view标签作用一致。
- template:向DSL插槽中插入一个模板片段
- field:字段元数据标签;data属性与元数据中
字段API名称(field)
对应。 - action:动作元数据标签;name属性与元数据中
动作API名称(name)
对应。
DSL模板
本质上是对布局模板
的补充,它使用布局模板
中已经定义好的DSL插槽
,对其进行相应规则的合并
、替换
,最终构成一个可被执行的视图模板
再经过客户端
渲染展示给用户。单独解释DSL模板
的结构化含义是完全没有意义的。
接下来,我们会分别定义不同的DSL模板
,并结合默认表格视图
的布局模板进行详细说明。
模板编译
服务端模板编译基础
进入页面时,客户端
将通过加载跳转动作(ViewActionLoad)
接口获取对应的布局模板和DSL模板,得到经过服务端编译
的JSON
数据,类似于如下数据结构:(DSL模板的JSON结构)
{
"dslNodeType": "VIEW",
"type": "TABLE",
......,
"widgets": [
{
"dslNodeType": "TEMPLATE",
"slot": "search",
"widgets": [
......
]
},
{
"dslNodeType": "TEMPLATE",
"slot": "actions",
"widgets": [
......
]
}
]
}
服务端编译过程可以简单的理解为以下几个内容:(实际要复杂很多,在这里我们仅需要关注最终拿到的JSON结果即可)
XML
转换为JSON
结构XML
标签转换为dslNodeType
属性,XML子标签
转换为widgets
属性,其他属性保持不变- 将
field标签(字段)
和action标签(动作)
对应的元数据补充完整。如上所示,我们仅定义了字段的data属性,返回的JSON中将包含字段
或动作
的全部元数据。字段
会补充字段类型、显示名称、是否存储等元数据属性,动作
会补充动作类型、显示名称等元数据属性。
示例模板
表格视图-布局模板
<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>
表格视图-DSL模板
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
<template slot="search">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</template>
<template slot="actions">
<action name="redirectCreatePage" />
</template>
<template slot="fields">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</template>
<template slot="rowActions">
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</template>
</view>
以上所示的两个模板(布局模板
和DSL模板
)会作为布局和DSL的模板合并
相关内容介绍的演示模板。更多的布局模板点击查看
布局和DSL的模板合并
当我们从服务端
获取了布局模板
和DSL模板
对应的JSON
后,客户端
将对这两个JSON
进行合并
处理。
合并
操作主要是将DSL模板
中定义的根标签
下定义的所有template标签
,向布局模板
中定义的DSL插槽
进行属性合并和子节点替换。
标准合并
将上述两个模板进行合并后,将得到如下结果:(为了方便观察结构,将使用XML结构进行解释,实际运行时为JSON结构)
<view type="TABLE">
<pack widget="group">
<view type="SEARCH">
<element widget="search" slot="search">
<!-- slot="search" -->
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</element>
</view>
</pack>
<pack widget="group" slot="tableGroup">
<element widget="actionBar" slot="actionBar">
<!-- slot="actions" -->
<action name="redirectCreatePage" />
</element>
<element widget="table" slot="table">
<element widget="expandColumn" slot="expandRow" />
<!-- slot="fields" -->
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
<element widget="rowActions" slot="rowActions">
<!-- slot="rowActions" -->
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</element>
</element>
</pack>
</view>
由此可见,DSL模板定义的template标签下的内容被合并到了布局模板的对应位置中。接下来就可以进行模板渲染操作了。
子标签替换和属性合并
一般的,我们将处理布局模板中任何标签上的slot属性,以及xslot标签。
遵循就近原则,模板合并将取路径最短的模板进行替换和插入。
当我们需要设置表格所有字段都允许排序时,我们可以使用sortable="true"
属性,设置在widget="table"
组件上。
使用slot="fields"
插槽是无法向widget="table"
组件添加额外属性的,此时我们需要使用slot="table"
这个插槽来实现我们的需求。
一个错误的DSL模板可能被定义成如下内容:(下面的示例舍弃了部分插槽内容)
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
<template slot="table" sortable="true">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</template>
<template slot="rowActions">
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</template>
</view>
在使用相同的布局模板情况下,合并后的结果为:
<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" />
<element widget="table" slot="table" sortable="true">
<!-- slot="table" -->
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</element>
</pack>
</view>
由此可见,我们优先使用了widget="table"
上的slot="table"
属性作为插槽,将原本定义在widget="table"
下的所有节点全部换成了在DSL模板中定义的内容,并且成功的添加了sortable="true"
这一属性。
但是,由于子标签被完全替换了,因此丢弃了widget="rowActions"
组件,导致slot="rowActions"
中定义的内容,找不到对应的插槽,这部分DSL模板内容将被丢弃。
反向合并
由于上面的合并操作丢弃了widget="rowActions"
组件,但我们的本意并不是想要丢弃这个组件,而是仍然希望能以某种规则将这部分内容继续保留。为了解决这个问题,我们提供了反向合并
操作,但前提是,DOM结构必须是同一层级
。
基于这一特性,我们将上述模板稍加修改:(将slot="rowActions"
放在slot="table"
下面)
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
<template slot="table" sortable="true">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
<template slot="rowActions">
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</template>
</template>
</view>
在使用相同的布局模板情况下,合并后的结果为:
<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" />
<element widget="table" slot="table" sortable="true">
<!-- slot="table" -->
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
<element widget="rowActions" slot="rowActions">
<!-- slot="rowActions" -->
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</element>
</element>
</pack>
</view>
由此可见,原本定义在布局模板上的widget="rowActions"
被找回来了,并且正确填充了定义在DSL模板中的内容。
不足的是,由于widget="table"
下面的所有内容都被替换了一次,我们无法确定原本的widget="rowActions"
组件应该如何正确放置,因此使用了DSL模板中定义的位置。
这可能会带来一些弊端,比如:将某一元素必须被放在第一个节点处。针对这种情况,布局模板本来是可以做到限定作用的,但由于使用了更大范围的插槽,使得这一特性丢失。
但也带来了一些灵活性,比如:大多数情况下,某一元素需要放在第一个节点处,但在特殊情况下,该元素需要放在最后一个节点处。
我们希望插槽可以设计的尽可能的灵活,以满足各种不同场景的需求。
属性插槽
当我们希望使用属性合并,但不希望替换子标签时,我们可以使用属性插槽
来解决这个问题。
当我们希望向widget="group"
组件上添加title="这是一个示例标题"
属性时,我们可以使用slot="tableGroup"
插槽。
我们可以使用如下所示的方式来定义DSL模板:
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
<template slot="tableGroup" title="这是一个示例标题" />
<template slot="table" sortable="true">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
<template slot="rowActions">
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</template>
</template>
</view>
在使用相同的布局模板情况下,合并后的结果为:
<view type="TABLE">
<pack widget="group">
<view type="SEARCH">
<element widget="search" slot="search" />
</view>
</pack>
<pack widget="group" slot="tableGroup" title="这是一个示例标题">
<element widget="actionBar" slot="actionBar" />
<element widget="table" slot="table" sortable="true">
<!-- slot="table" -->
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
<element widget="rowActions" slot="rowActions">
<!-- slot="rowActions" -->
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</element>
</element>
</pack>
</view>
由此可见,当DSL模板中的template
标签下没有任何子标签时,我们仅会处理属性合并
,但不会处理在原布局模板下定义的子标签。
根据这一特性,我们也可以定义这样的DSL模板来实现上面给widget="table"
添加sortable="true"
的需求:
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
<template slot="tableGroup" title="这是一个示例标题" />
<template slot="table" sortable="true" />
<template slot="fields">
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
</template>
<template slot="rowActions">
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</template>
</view>
在使用相同的布局模板情况下,合并后的结果为:(与之前的合并结果完全一致)
<view type="TABLE">
<pack widget="group">
<view type="SEARCH">
<element widget="search" slot="search" />
</view>
</pack>
<pack widget="group" slot="tableGroup" title="这是一个示例标题">
<element widget="actionBar" slot="actionBar" />
<element widget="table" slot="table" sortable="true">
<!-- slot="table" -->
<field data="id" invisible="true" />
<field data="name" />
<field data="isEnabled" />
<element widget="rowActions" slot="rowActions">
<!-- slot="rowActions" -->
<action name="redirectUpdatePage" />
<action name="enable" />
<action name="disable" />
</element>
</element>
</pack>
</view>
由于属性插槽
本身没有子标签,因此我们建议将所有的属性插槽
配置在view标签
子标签的最前面进行统一定义。
模板渲染
模板渲染是将一个包含dslNodeType
属性的对象,生成对应框架的VDom对象
,通过该VDom对象
执行一段创建ts组件
的逻辑,并通过ts组件
渲染该组件对应的VDom对象
。
例如:使用Vue框架
时,该VDom对象
通常为VNode
。
(以下内容均围绕Vue框架
进行描述,其他框架基本一致)
为了将模板渲染尽可能的接近第三方框架的渲染逻辑,我们采用了Tag(Vue组件) - Class Component(ts) - Component(Vue组件)
这样的三层结构进行模板渲染的实现。
- Tag(Vue组件):使用
resolveComponent
方法,将一个包含dslNodeType
属性的对象,通过既定规则进行解析,获取一个Vue组件作为入口。主要处理模板标签和Vue组件之间的对应关系。 - Class Component(ts):使用ts面向对象的特性,可以对组件逻辑进行重载、继承等处理,增加组件的灵活性。
- Component(Vue组件):主要实现数据渲染。
具体的渲染逻辑不在此处进行介绍,该篇文章主要是为了让读者对模板以及整体框架有一个基本的概念,使用这套模板渲染的最终目的是为了让组件复用率更高,开发更加灵活。
Oinone社区 作者:oinone原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/23.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验