低代码越来越火,争议也是不断,其实低代码这种方式,二十年前就有了,只是不叫这个名字,因为不想偷懒的程序员不是好架构师!
牛顿和爱因斯坦都说过:要站在巨人的肩膀上。
现在Vue、UI库等都很强大了,我们要做低代码的话,已经不用从零开始,而是可以站在巨人的肩膀上,使用各种开源项目,实现自己的低代码!
第一步:选一个前端框架,比如 vue3,当然其他也可以。
第二步:选一个UI库进行二次封装,比如 elementPlus,当然其他也可以。
第三步:制定一套 json。
第四步:做一个维护 json 的支撑平台,可视化,可拖拽的那种,拒绝手撸 json。
第五步:做各种扩展,比如审批流。
为啥要二次封装UI库?
为了实现低代码的时候可以更方便!
UI库 一般都很注重灵活性,所以方便性就差了一点点,我们可以根据自己的需求,牺牲一些灵活性,换取便捷性。
举个例子,看看实现表单的时候,二者需要的代码量的对比。
封装前
比如UI库的表单组件(以及表单里面的子组件),直接使用是这样的:(官网示例的截图)
功能很强大也很灵活,只是需要的代码有点长。。。
二次封装后
但是封装之后呢,就可以这样使用:(表单控件的代码,使用二次封装的表单子组件的方式)
业务组件里使用表单控件
<nf-form
v-form-drag="formMeta"
:model="model"
:partModel="model2"
v-bind="formMeta"
>
<!--支持插槽-->
</nf-form>
- v-form-drag:实现拖拽功能。
- model:直接传入model对象。
- partModel:选择后对应的表单数据。
- v-bind:绑定其他属性。
- 可以使用slot,扩展表单里的子组件。
直接一个表单控件就可以搞定一个表单需求,当然需要配上一个 json 文件。比使用原生的表单组件,要节省很多代码。
表单控件的内部代码
那么这个表单控件内部是什么样子?
给 el-form 设置属性,加载子控件,并且设置需要的属性。
<el-form
:model="model"
ref="formControl"
:inline="false"
class="demo-form-inline"
:label-suffix="labelSuffix"
:label-width="labelWidth"
:size="size"
v-bind="$attrs"
>
<!--表单里面的子组件-->
<base-item
:colOrder="colOrder"
:itemMeta="itemMeta"
:ruleMeta="ruleMeta"
:showCol="showCol"
:formColSpan="formColSpan"
:model="model"
:size="size"
>
</base-item>
</el-form>
- base-item 表单子组件
<el-row :gutter="15">
<el-col
v-for="(ctrId, index) in colOrder"
:key="'form_' + ctrId + '_' + index"
:span="formColSpan[ctrId]"
v-show="showCol[ctrId]"
>
<transition name="el-zoom-in-top">
<el-form-item
:label="itemMeta[ctrId].meta.label"
:prop="itemMeta[ctrId].meta.colName"
:rules="ruleMeta[ctrId] ?? []"
:label-width="itemMeta[ctrId].meta.labelWidth??''"
:size="size"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[itemMeta[ctrId].meta.controlType]"
:model="model"
:meta="itemMeta[ctrId].meta"
v-bind="itemMeta[ctrId].props"
>
</component>
</el-form-item>
</transition>
</el-col>
</el-row>
- el-row:可以做多列表单。
- transition:显示、隐藏字段时,可以做过渡动画。
- component:动态组件,根据类型加载对应的子组件,比如 input、select等
是不是方便多了?
那么如何实现呢?我们要先定义一套 Interface!
为啥要先定义 Interface ?
接口有很多好处,比如:
- 可以当文档用,有多少属性,名称、类型都可以在接口里体现,还可以写点注释。
- 可以统一操作风格。比如 el-input、el-select等,在使用的时候,画风不一致,这导致在form里不方便使用 v-for,使用接口可以统一操作。
- 可以兼容各种UI库。
表单子组件的 接口定义
一个表单需要各种各样的子组件,比如 input、select等,我们需要对这些子组件定义一套统一的 Interface:
/**
* 表单控件的子控件的 props。
*/
export interface IFormItemProps<T extends object> {
meta: IFormItemMeta,
model: T,
optionList?: Array<IOptionList | IOptionTree>,
clearable?: boolean,
}
分为三大部分:
- meta:二次封装需要的属性。
- model:表单的 model 对象,需要被各种传递,使用泛型的方式定义类型。
- UI库需要的属性
- optionList:备选项需要的数据,比如 select 的 option。
- clearable:需要显示清空按钮
使用泛型定义 model 的类型,这样更灵活。
一开始的定义方式是把所有的属性都放在一起,都是第一个级,但是使用的时候感觉有点乱,分不清归属。
后来定义一个 other 属性,统一存放UI库的属性,但是发现也不方便。
最后决定:
- 二次封装需要的属性统一放在 meta 里面(作为二级属性)
- 而UI库的组件需要的属性,都放在第一级,这样便于扩展。
meta
我们来看看 meta 里面需要的属性
/**
* 二次封装时需要的属性
*/
export interface IFormItemMeta {
columnId?: idType,
colName: string,
label?: string,
controlType?: EControlType | number,
defValue?: any,
colCount?: number,
webapi?: IWebAPI,
delay?: number,
events?: IEventDebounce,
}
- columnId:字段ID、控件ID。对于前端的tx来说,这种ID会比较陌生,但是这个ID对于后端来说非常重要,对于低代码来说,也非常重要。
- colName:字段名称,即 model 里的属性名称。
- label:字段的中文名称,标签。
- controlType:子控件类型,比如 input、select 等。
- defValue:子控件的默认值。
- colCount:一个控件占据的空间份数,用于多列表单。
- webapi:访问后端API的配置信息,有备选项的控件需要。
- delay: 防抖延迟时间,0:不延迟。
UI库的组件需要的属性
并不需要把所有的属性都定义为接口的属性,这工作量就太大了,我们只需要定义几个即可。
放进来的属性的规则:
- 需要设置默认值的属性。
- 需要在代码里面操作的属性。
比如UI库的组件,默认不带清空数据的按钮。我想都统一带上,于是定义了 clearable 属性,并且设置默认值为 true,这样就都统一带上清空按钮了。
比如表单里一些组件需要使用备选项,那么就统一定义接口,所以备选项都按照这个接口做 v-for。
可以用在 select、checkbox、radios 的 备选项
/**
* 单层的选项,下拉列表等的选项。value、label
*/
export interface IOptionList {
value: number | string,
label: string,
disabled?: boolean
}
- value:提交到后端的值,可以是数字或者文本
- label:标签、文字说明
- disabled:是否可用
可以用于多级的备选项。
/**
* 多级的选项,级联控件。value、label、children(n级嵌套)
*/
export interface IOptionTree {
value: idType,
label: string,
disabled: boolean,
children: Array<IOptionTree>
}
- value:提交到后端的值,可以是数字或者文本
- label:标签、文字说明
- disabled:是否可用
- children:嵌套的方式实现多级分类
Interface 的用途
有了接口定义,相当于制定了一份规则:
- 准备数据的时候,可以按照这个接口设置 json 文件。
- 封装组件的,可以按照这个接口编写代码。
- 可以支持多种UI库,其他UI库,也按照这套 Interface 进行封装。
属性名称对不上的话,就以接口为准。
封装其他UI库的时候,也按照这套接口来,这样 json 文件就可以用于支持(渲染)各种UI库了,
举例
比如封装一个文本框
<script setup lang="ts" generic="T extends object">
// 引入接口
import type { IFormItemProps } from '../map'
// 定义 props 和设置默认值
const props = withDefaults(defineProps<IFormItemProps<T>>(), {
meta: () => {
return {
colName: ''
}
},
model: () => {
return {} as T
},
clearable: true
})
</script>
组件的主要工作:
- 引入Interface
- 定义 props
- 设置默认值
<el-input
v-model="model[colName]"
v-bind="$attrs"
:id="'c' + meta.columnId"
:name="'c' + meta.columnId"
:clearable="clearable"
>
</el-input>
使用 UI库提供的 el-input,设置需要的属性,使用 $attrs “透传”其他属性。
直接 使用 model[colName]
绑定 v-model,拒绝墨迹。
选择框
<script setup lang="ts" generic="T extends object">
import type { IFormItemProps } from '../map'
// 定义 props
const props = withDefaults(defineProps<IFormItemProps<T> & {
collapseTags: boolean,
collapseTagsTooltip: boolean
}>(), {
meta: () => {
return {
colName: ''
}
},
model: () => {
return {} as T
},
clearable: true,
collapseTags: true, // 多选
collapseTagsTooltip: true
})
</script>
可以增加 props 里面的属性。
<el-select
v-model="model[colName]"
v-bind="$attrs"
:id="'c' + meta.columnId"
:name="'c' + meta.columnId"
:clearable="clearable"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTagsTooltip"
>
<el-option
v-for="item in optionList"
:key="'select' + item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
>
</el-option>
</el-select>
用 v-for 设置 el-option ,在表单里面v-for的时候,可以一视同仁了。
其他问题
上面只是简单的示例,虽然可用,但是功能不完善,比如防抖问题、一个组件对应多个字段的问题。
这还需要我们定义一个统一的管理函数来解决。会陆续介绍解决方案。
小结
这是二次封装UI库的基础想法,写的有点乱,不知道大家有没有看懂,如果没看懂的话,欢迎提问,后续我会介绍各种细节。