【手撸低代码工具】二次封装UI库(一)简单介绍一下想法

860 阅读2分钟

低代码越来越火,争议也是不断,其实低代码这种方式,二十年前就有了,只是不叫这个名字,因为不想偷懒的程序员不是好架构师!

牛顿和爱因斯坦都说过:要站在巨人的肩膀上。

现在Vue、UI库等都很强大了,我们要做低代码的话,已经不用从零开始,而是可以站在巨人的肩膀上,使用各种开源项目,实现自己的低代码!

第一步:选一个前端框架,比如 vue3,当然其他也可以。
第二步:选一个UI库进行二次封装,比如 elementPlus,当然其他也可以。
第三步:制定一套 json。
第四步:做一个维护 json 的支撑平台,可视化,可拖拽的那种,拒绝手撸 json
第五步:做各种扩展,比如审批流。

为啥要二次封装UI库?

为了实现低代码的时候可以更方便!

UI库 一般都很注重灵活性,所以方便性就差了一点点,我们可以根据自己的需求,牺牲一些灵活性,换取便捷性。

举个例子,看看实现表单的时候,二者需要的代码量的对比。

封装前

比如UI库的表单组件(以及表单里面的子组件),直接使用是这样的:(官网示例的截图)

600el-form官网.jpg

功能很强大也很灵活,只是需要的代码有点长。。。

二次封装后

但是封装之后呢,就可以这样使用:(表单控件的代码,使用二次封装的表单子组件的方式)

业务组件里使用表单控件

<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库的基础想法,写的有点乱,不知道大家有没有看懂,如果没看懂的话,欢迎提问,后续我会介绍各种细节。