组件SPI机制(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为根节点的树:

image.png

``` mermaid
graph TD
BaseFieldWidget ---> FormStringFieldWidget
BaseFieldWidget ---> FormStringMultiFieldWidget
FormStringFieldWidget ---> FormStringHyperlinksFieldWidget
```

树的构建

上述形成的组件树实际并非真实的存储结构,真实的存储结构是通过维度进行存储的,如下图所示:

(圆角矩形表示维度上的属性和值,矩形表示对应的组件)

image.png

``` mermaid
graph TD
viewType([viewType: ViewType.Form]) --->
ttype([ttype: ModelFieldType.Strng]) --->
multi([multi: true]) & widget([widget: 'Hyperlinks'])

direction LR
ttype ---> FormStringFieldWidget
multi ---> FormStringMultiFieldWidget
widget ---> FormStringHyperlinksFieldWidget
```

有权重的最长路径匹配

同样以上述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:组件名称

标签组件概览

为了提供更好的灵活性,平台提供的所有标签组件,均支持classstyle属性,在无法满足业务需求的情况下,可以使用这些特性进行处理。

标签 描述
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-widget1my-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低代码应用平台体验

(1)
oinone的头像oinone
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • 【界面设计器】自定义字段组件实战——表格字段内嵌表格

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 表格中的一对多(O2M)或多对多(M2M)字段使用表格展开。 演示内容:在【商品】的表格中存在【库存信息】这一列,这一列的内容通过表格展示【尺码】和【数量】两列。 业务分析及实现思路 从需求来看,我们需要实现一个【内嵌表格】组件,并且该组件允许在【表格】视图中使用。与之前不同的是,这里我们需要支持两个业务类型一对多(O2M)和多对多(M2M),即一个组件中包含两个元件。 在【内嵌表格】组件的属性面板中,我们需要再定义一个【内嵌表格配置】组件,用来选择内嵌表格中需要哪些字段进行组合,以及为每个组合提供一些基础配置。 这里需要理解一个基本概念,即【内嵌表格】的属性面板是【内嵌表格配置】的【执行页面】。所有组件的属性面板在【执行页面】时都是【表单】视图。 因此我们可以实现一个【内嵌表格配置】组件,并且该组件允许在【表单】视图中使用。其业务类型使用【文本】,我们在保存配置数据时,可以使用JSON数据结构来存储复杂结构。(这里的实现思路并非是最符合协议设定的,但可以满足绝大多数组件场景) 在【内嵌表格配置】组件中,我们可以允许用户添加/移除组合,并且每个组合有两个属性,【标题】和【字段】。 一些解释 看过【界面设计器】自定义字段组件实战——表格字段组合展示文章的读者可能很熟悉这一实现思路,会想当然的尝试将两个组件进行合并。这里我觉得有必要作出一些实现思路上的解释。 虽然在表面上看起来【组合列配置】和【内嵌表格配置】用到的属性完全一样,但在实现上,由于关联关系的查询需要在组件中特殊处理【透出字段(选项字段列表)】字段(【界面设计器】组件开发常见问题中对该属性进行了解释),才能查询到相应的关联数据。 不仅如此,这两个组件所代表的含义也完全不同。【组合列配置】是在一列中配置需要展示的字段,它在未来可能会增加【颜色(根据条件判断展示不同的颜色)】、【动作(可点击的行为)】等等诸多与之相关的属性。【内嵌表格配置】是在一列中配置表格中的多列,它在未来可能会增加【行高(控制表格行高)】、【支持排序(表格列支持排序)】等等诸多与之相关的属性。 在这里希望读者可以理解一点:相似并不代表相关。组件的抽象与归纳整理的不同点在于,抽象更需要关心其本身所代表的含义,而不是仅关注其相似程度。将多个相似度高但含义不同的组件进行归纳整理得到的只是一个含义不明,无法适应变化的组件。 因此,我们仍然使用两个不同的组件进行实现。 准备工作 此处你应该已经准备好了【商品】和【库存】两个模型,并且可以完整执行【商品】模型的全部【增删改查】操作。 业务模型定义(此处模型定义并非业务中正常使用的模型定义,仅作为演示使用) (以下仅展示本文章用到的模型字段,忽略其他无关字段。) 关联字段:-左侧表示当前模型中的字段API名称,右侧表示关联模型中的字段API名称。 商品(Item) 名称 API名称 业务类型 是否多值 长度(单值长度) 关联模型 关联字段 ID id 整数 否 – – – 编码 code 文本 否 128 – – 名称 name 文本 否 128 – – 库存信息 inventoryInfo 一对多 是 – 库存(Inventory) id – itemId 库存(Inventory) 名称 API名称 业务类型 是否多值 长度(单值长度) 关联模型 关联字段 ID id 整数 否 128 – – 商品 item 多对一 否 – 商品(Item) itemId – id 商品ID itemId 整数 否 – – – 尺码 size 文本 否 128 – – 库存 count 整数 否 – – – PS:如果是使用【模型设计器】创建这两个模型,在创建关联关系字段时必须使用双向关联,才能正确建立关联关系。 实现页面效果展示 表格视图 表单视图 库存信息配置 创建组件、元件 准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。 以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。 创建内嵌表格组件 创建内嵌表格元件(一对多) 创建内嵌表格元件(多对多) 创建内嵌表格配置组件 创建内嵌表格配置元件 设计内嵌表格元件属性面板 (两个元件的属性面板可以完全一致) 创建tableConfig字段,并切换至【内嵌表格配置】组件。 再拖入【透出字段(选项字段列表)】,并设置为隐藏。 设计内嵌表格配置元件属性面板 启动SDK工程进行组件基本功能开发 开发步骤参考 打开【表格】视图,将【库存信息】字段的组件切换为【内嵌表格】 在属性面板中看到【内嵌表格配置】组件,并优先实现【内嵌表格配置】组件。这里的属性面板就是【内嵌表格配置】组件对应的【执行页面】。 当【内嵌表格配置】组件可以按照预先设计的数据结构正确保存tableConfig属性时,可以在【内嵌表格】组件中的props定义中直接获取该属性,接下来就可以进行【内嵌表格】组件的开发。 代码实现参考 工程结构 typing.ts export interface InlineTableConfig { key: string; label?: string; field?: string; } FieldService.ts import { GenericFunctionService, IModelField, QueryPageResult } from '@kunlun/dependencies'; export class FieldService { private static readonly FIELD_MODEL = 'base.Field'; public static async fetchFieldsByModel(model: string, filter?: string): Promise<IModelField[]> { let rsql = `model == "${model}"`; if (filter) {…

    2023年11月1日
    1.1K00
  • 登录页自定义配置如何使用

    介绍 为满足大家对登录页面不同的展示需求,oinone在登录页面提供了丰富的配置项以支持大家在业务上的个性化需求 配置方式 在manifest.js内新增以下配置选项 manifest.js文件如何配置的参考文档 runtimeConfigResolve({ login: { /** * 登录按钮label */ loginLabel: '登录', /** * 是否显示忘记密码按钮 */ forgetPassword: true, /** * 忘记密码按钮Label */ forgetPasswordLabel: '忘记密码', /** * 是否显示注册按钮 */ register: true, /** * 注册按钮Label */ registerLabel: '注册', /** * 是否显示验证码登录Tab */ codeLogin: true, /** * 账号登录Tab Label */ accountLoginLabel: '账号', /** * 验证码登录Tab Label */ codeLoginLabel: '验证码', /** * 账号登录-账号输入框Placeholder */ accountPlaceholder: '请输入账号', /** * 账号登录-密码输入框Placeholder */ passwordPlaceholder: '前输入密码', /** * 验证码登录-手机号输入框Placeholder */ phonePlaceholder: '请输入手机号', /** * 验证码登录-验证码输入框Placeholder */ codePlaceholder: '请输入验证码', } });

    2024年4月24日
    1.5K00
  • TableWidget 与 FormWidget 浅析

    前言:本文主要聚集于 TableWidget和 FormWidget,对两者以外的内容不做赘述。 TableWidget和 FormWidget作为一个基本的渲染模块与 Mask等不同,TableWidget与 FormWidget有着明确的目的,比如 TableWidget就是作为一个表格视图,这种名称中可以看得出来。其将表格的一系列能力聚拢,如单元格,行列选择排序,翻页等等; TableWidget 我们使用表格到底是在使用什么? 在我们讲述渲染流程前,我们需要提一个问题,在我们使用表格组件时,我们在使用什么,换而言之,我们对表格组件中关注的是什么?应该说我们更关注的是表格所展示的数据。TableWidget或者说整个低代码其实解决的就是这个问题,让我们可以很方便的展示数据。不用关心一些细枝末节 TableWidget 的渲染 TableWidget在整个渲染流程中的负责组装各种 Vue 组件所需要的核心数据或事件回调并传递给其绑定的 Vue 组件即 DefaultTable,如 allowRowClick,rowClickMode等等,这些值会作为 Props 传递给 DefaultTable,DefaultTable则一定意义上充当了一个桥接层,主要做了两件事,处理 Props, 处理 Slot,TableWidget传递给 DefaultTable的 Props 会经过 DefaultTable再次组合,创建新的 Props,同时根据当前的 Props 判断是否有必要新增一些 slot,比如 OioPagination组件是否需要渲染就取决于 Props.showPagination 的值,经过 DefaultTable的处理后, props与 slot会进一步交给 OioTable进行渲染,OioTable则会进一步聚合处理,比如如果没有 defaultSlot则进行空值的渲染,如果存在 footSlot则会构建一个包裹层去包裹它。这些组件协同完成了一个表格的结构,而我们真正关心的数据则由一个个 BaseTableColumnWidget渲染。BaseTableColumnWidget的渲染过程类似于 TableWidget,其负责组装 DefaultTableColumn渲染所需要的 props,然后交给 OioTableColumn进行实际渲染。并且会有多种基于 BaseTableColumnWidget的 widget通过重写 renderDefaultSlot,renderEditSlot,renderContentSlot,renderHeaderSlot等几个 props 实现不同状态,不同类型等组件的渲染。通过 TableWidget与 BaseTableColumnWidget相结合, Table 页面完成了主体框架与内容的渲染。而当数据完成渲染后,不可避免的会有数据交互,比如分页,排序,过滤等,这些交互都由 TableWidget与 BaseTableColumnWidget共同完成。比如排序翻页等,TableWidget会将事件作为 props 向下注入,当事件被调用,TableWidget会进行处理,比如发起请求,或者对内部数据排序等。而除了数据的展示,还有一些动作,即 Action ,Action 被触发时会按照内部的配置进行工作,比如编辑时,将获取 activeRecord,随后触发 Table 的编辑。 TableWidget就是这样去渲染出了一个完整的表格页面。能够完成数据的增删改查等操作 FormWidget FormWidget与 TableWidget一样,也是作为一个渲染模块,但是它与 TableWidget不同的是,FormWidget是作为一个表单视图,其将表单的一系列能力聚拢,如表单提交,表单校验,表单重置等等。其重点在于对数据的增改。所以在提供的能力上也有区别,比如 FormWidget没有提供翻页,排序,过滤等能力,因为这些能力属于表格的能力,而 FormWidget则更关注于表单的能力。提供了数据的存储,提交,校验等能力 Form 在渲染时流程与 Table 大同小异,其同样为三层结构 FormWidget => DefaultForm => FormFieldWidget 一层层向下渲染,不同的在于 FormWidget 更多的关注点在于维护其内部的 FormData 这是整个表单页面所围绕的东西,当页面上的控件发生变化,其变更的值会被收集到 FormData 中,并在后续中使用。同时在编辑已有数据场景下,Form 会将数据加载到 FormData,随后下放给 FormFiledWidget。 异同之处 从介绍中可以看出,Table 侧重于数据的查询展示,Form 则侧重于数据的变动处理,但是抽象的看其核心其实是同一套逻辑,即数据的存储与展示,中间或许会有对数据的某些处理,但是并不是本质上的区别,两者在核心理念上是一致的,即让使用者只需要关心数据本身,而不需要关注于繁琐的视图构造,这是整个低代码甚至前端的发展方向。

    2025年3月25日
    74000
  • 从一个方法开始:浅析页面渲染流程

    渲染前的准备 渲染前的准备,在 Vue 渲染框架下,会先安装所有所支持的默认组件,比如 Mask,Header 等,这些组件支持 XML 默认模版的 Vue 框架下的渲染,详情可见 main.ts 中,maskInstall 与 install,这两个函数将平台内部支持的组件进行了注册,随后将整个 Vue 挂载为运行时 App,随后进行初始化。 渲染的起步 OioProvider 方法是整个应用的入口,我们忽略掉一些配置方法,将注意力集中到 initializeServices 从名称中我们可以看出来内部保存的都是初始化服务,其中提供了渲染服务等,我们当前使用的是 Vue 框架,所以当前其渲染的 Root 节点为 Vue, 以此,我们视野可以暂时转移到 admin-base中的 Root.vue以及 RootWidget上, 其实现了整个 Vue 框架下的 Root 节点如何渲染,其中定义了多个 widget,比如登陆页,首页,忘记密码已经重置密码等页面, 在本文中我们着重关注渲染首页的能力,RootWidget将 DefaultMetadataMainViewWidget作为渲染 Props中的 page即首页提供给下层组件使用, 渐入佳境 DefaultMetadataMainViewWidget从名称中可以看到,其为元数据主视图,主要做两件事,1:提供 Mask 的渲染2:提供元数据上下文初始化 该组件主要通过观察路由变化触发上面两个动作,当路由发生变化,该组件 reloadPage将被调用,reloadPage方法通过组装路由参数构成一个唯一 key 向后段查询当前路由所对应的渲染信息, 随后将获取到的信息进行处理,初始化,即 元数据上下文初始化,在初始化后,会将获取到的数据注入到 MetadataViewWidget中, Mask 的渲染 关于 Mask 的渲染,在获取到数据后,将生成 maskTemplate,并将其赋值, DefaultMetadataMainView.vue文件将渲染该模板,并渲染到页面中 数据的变更 当上面两件任务完成后,将开始主视图的渲染,上文提到,DefaultMetadataMainViewWidget只负责 mask 的渲染和上下文的初始化,所以 DefaultMetadataMainViewWidget通过触发事件的方式来实现主视图的渲染, DefaultMetadataMainViewWidget将必要信息作为事件参数触发,MultiTabsContainerWidget接收到 reloadMainViewCallChaining事件后,开启主视图渲染, MultiTabsContainerWidget会刷新运行时上下文,即 refreshRuntimeContext,该方法将尝试查询并通过 createOrUpdateEnterTab方法创建 Tab 页,createOrUpdateEnterTab最终生成一个 MultiTabItem格式的对象,该对象描述了 Tab 的相关信息,随后调用 createTabContainerWidget创建 tab 的容器即新建了一个 MultiTabContainerWidget组件即单个 tab 的容器,随后调用 setActiveTabItem, 并获取其绑定的 Vue 组件,并将其组件放置在 KeepAlive内部,触发更新, 主视图的渲染 MultiTabContainerWidget继承自MetadataViewWidget,当 MetadataViewWidget数据发生变更, 其绑定的 Vue 组件将解析 viewTemplate, 获取到与该模板 dslNodeType想匹配的 Vue 组件,当前例子中为 View.vue,随后 View.vue开始渲染,View.vue文件只是一个纯粹的容器,当 View.vue被挂载时,其内部的 template属性包含了整个页面的描述信息,View.vue需要做的就是将这个 template翻译并渲染成 DOM 展现在浏览器上, 渲染整个页面 当 View.vue被挂载时,其内部的 template属性包含了整个页面的描述信息,View.vue主要做了两个事情,一:将 template 中的 widget转换为组件,二:根据当前的 template信息生成 slot信息, const currentSlots = computed<Slots | undefined>( () => DslRender.fetchVNodeSlots(props.dslDefinition) || (Object.keys(context.slots).length ? context.slots : undefined) ); const renderWidget = createCustomWidget(InternalWidget.View, { …context.attrs, type: props.type || viewType.value, template: props.dslDefinition, metadataHandle: props.metadataHandle || metadataHandle.value, rootHandle: props.rootHandle || rootHandle.value, parentHandle: props.parentHandle || parentHandle.value, slotName: props.slotName, inline: inlineProp } as ViewWidgetProps); 生成这两部分信息后,View.vue会将这两部分挂载到页面上,这两部分从代码中可以看出,主要靠 fetchVNodeSlots,createCustomWidget两个函数, export function createCustomWidget( widget: string, props: CustomWidgetProps ): RenderWidget | undefined public static fetchVNodeSlots(dsl: DslDefinition | undefined, supportedSlotNames?: string[]):…

    2025年3月19日
    54100
  • 【界面设计器】自定义字段组件实战——千分位输入框

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 用户在输入【金额】字段时,实时展示千分位格式。 业务分析 从需求来看,我们需要实现一个【千分位】组件,并且该组件允许在【表单】视图中使用。 扩展实现:该组件虽然仅要求在【表单】中使用,但也可以在【搜索】中使用完全相同的实现,因此这里我们在注册时会增加【搜索】视图,将【千分位】组件应用在【搜索】中。对于【表格】、【详情】和【画廊】来说,该组件是没有【输入】行为的展示组件,在这里我们不进行演示。 准备工作 此处你应该已经在某个业务模型下,可以完整执行当前模型的全部【增删改查】操作。 业务模型定义 (以下仅展示本文章用到的模型字段,忽略其他无关字段。) 名称 API名称 业务类型 是否多值 长度(单值长度) 编码 code 文本 否 128 名称 name 文本 否 128 金额 money 金额 否 – 创建组件、元件 准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。 以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。 创建千分位组件 创建千分位元件 启动SDK工程进行组件基本功能开发 (npm相关操作请自行查看SDK工程中内置的README.MD) 关键点详解 数据交互类型的字段组件(以下简称展示组件)与仅展示类型的字段组件(以下简称交互组件)有一些差别。 通常情况下,在展示组件中仅需使用value属性即可展示相关内容。在交互组件中除了value用于展示外,还需使用change、focus以及blur处理用户输入时的基本交互逻辑。 数据交互方法主要功能说明: change方法:当值发生变更时调用,根据字段相关配置将值回填至表单中。 focus方法:当组件获取焦点时调用,记录当前值,并在调用blur方法时进行处理。 blur方法:当前组件失去焦点时调用,根据focus方法记录的值,进行失焦触发逻辑的执行。 这里的三个数据交互方法仅仅是对用户行为的抽象,而并非限定其使用场景。 通常来说,这三个方法的调用顺序为:focus –> change –> blur。 如在日期时间组件中,面板打开时调用了focus方法,面板值发生变更时,调用了change方法,面板关闭时调用了blur方法。 如在地图组件中,选中地图上的某个点时仅会调用change方法,用户交互上并不能体现出focus和blur的行为。因此对于这个组件而言,只会有change方法被执行。 代码实现参考 PS:oio-input-number样式必须升级到4.6.x以上的最新版本才支持 Thousandth.vue <template> <a-input-number class=”oio-input-number” :value=”inputValue” :formatter=”formatter” :parser=”parser” @update:value=”change” @focus=”focus” @blur=”blur” /> </template> <script lang=”ts”> import { InputNumber as AInputNumber } from ‘ant-design-vue’; import { computed, defineComponent } from ‘vue’; export default defineComponent({ name: ‘Thousandth’, components: { AInputNumber }, props: { value: { type: [String, Number] }, change: { type: Function }, focus: { type: Function }, blur: { type: Function } }, setup(props) { const inputValue = computed(() => { return props.value; }); const formatter = (value) => { return <span class="hljs-subst">${value}</span>.replace(/\B(?=(\d{3})+(?!\d))/g, ‘,’); }; const parser = (value) => { return value.replace(/\$\s?|(,*)/g, ”); }; return { inputValue, formatter, parser }; } }); </script> FormMoneyThousandthFieldWidget.ts import { FormFieldWidget, ModelFieldType, SPI, ViewType } from ‘@kunlun/dependencies’; import Thousandth from ‘./Thousandth.vue’; @SPI.ClassFactory( FormFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.Currency,…

    2023年11月1日
    95000

Leave a Reply

登录后才能评论