开源 | Lemon-form(柠檬轻表单) 基于Vue3实现的开源表单系统

881 阅读5分钟

Lemon-Form(柠檬轻表单)是一款类似金数据飞书表单的动态表单系统,只在解决用户可以通过拖拉拽的交互方式创建动态表单。

Lemon-Form表单最初是为了解决客户定制表单页面的开发需求,通过抽象和沉底成一套可复用的动态表单系统,通过无代码的方式,快速搭建出一套成熟的表单功能,最终可以在此系统上完成表单的创建,数据的收集,数据清洗和数据分析。

image.png

在线地址

GitHub项目地址

(一)动态表单实现原理

动态表单的实现原理:基于JsonSchema动态渲染组件,核心在于解耦表单结构配置与组件UI呈现。

  1. 数据驱动模型:表单页面的UI显示,逻辑交互,数据绑定和表单提交,都是通过配置完成。动态渲染成DOM,而无需直接操作DOM。
  2. 渲染器:在表单渲染器上,我们采用动态组件component的渲染方式,根据表单类型动态渲染不同的配置组件元素。
  3. 拖拉拽:在交互方式上,我们采用VueDraggable拖拉拽完成了组件的创建和顺序的更新,降低表单搭建的使用成本

(二)动态渲染器实现原理

渲染器的介绍

简单来说渲染器是一个函数:根据表单的配置动态渲染出UI的业务逻辑,并动态完成配置的逻辑配置,数据绑定,交互方式。

渲染器是动态表单的核心模块之一,也是类似低代码产品的核心模块,一般渲染渲染器涉及到三个不同的业务场景:

  1. 开发画布渲染:开发状态画布渲染,涉及到用户动态配置,拖拉拽用户调整导致的动态渲染逻辑,复杂一些的包括多人协同导致的算法调度和实时渲染逻辑,功能最全,最复杂
  2. 预览:针对开发态的页面进行实时预览,查看页面搭建效果,可以调整预览终端,页面大小,主题等元素并实时获取反馈。
  3. 线上页面渲染:根据用户请求直接获取页面并渲染,比预览更纯粹的动态渲染过程,针对用户是PC和h5,深背景等页面直出,一般不包括预览选项元素。

其中:针对不复杂的业务场景【预览模块】和【线上页面渲染】是一个功能模块完成。

(三)Lemon-Form 渲染器的实现原理

3.1 创建元素的渲染过程(v0.1)

我们先看一个空表单,创建一个元素的渲染逻辑:

  1. 首选创建一个用来存储表单元素的数组对象:pageCompList
  2. 创建一个元素,就会在pageCompList添加一个新的元素
  3. Ui部分通过遍历的方式动态渲染出pageCompList的元素到页面上

通过上面的业务逻辑,我们先提供一个v0.1版本的渲染器实现过程:

// 声明表单元素列表对象
const pageCompList = ref([])

// 添加元素
const add = (item) {
   pageCompList.value.push(item)
}

// 渲染逻辑
<div v-for="(item, index) in pageCompList">
   <input v-if="item.type === 'input'"/>
</div>

3.2 优化上面渲染过程(v0.2)

3.2.1 目前的问题

通过上面的代码,我们会发现一个问题:动态表单组件渲染过程,我们无论是通过v-if,v-show的渲染方式,我们需要添加很多的动态模版代码,一下场景都无法满足优雅的解决:

  1. 新增更多的组件,导致模版代码难以维护
 // v1 版本
 <input v-if="item.type === 'input'"/>
 
 // v2 版本
 <input v-if="item.type === 'input'"/>
 <select v-if="item.type === 'select'"/>
这里新增了 ...more 100+
  1. 配置调整导致模版维护成本
 // v1 版本
 <input v-if="item.type === 'input'"/>
 <select v-if="item.type === 'select'"/>
 
 // v2 版本,新增了VIP功能,表单全局配置了不允许操作,则disabled
 <input 
     v-if="item.type === 'input'" 
     :id="item.type + uuid()" 
     :disabled="formConfig.inputDisabled"/>
 <select v-if="item.type === 'select'" 
     :id="item.type + uuid()" 
     :disabled="formConfig.inputDisabled"/>
这里新增了 ...more 100+
  1. 组件升级导致维护成本
 // v1 版本
 <Input v-if="item.type === 'input'"/>
 
 // v2 版本,input组件,改用了Learn-Form-Input组件
 <Learn-Form-Input v-if="item.type === 'input'">
  1. 动态组件加载不支持:用户希望自己开发的插件可以低成本的集成,目前的方案一定需要调整配置页面,这个是有成本的。
 // v1 版本
 <input v-if="item.type === 'input'"/>
 // 客制化组件
 <custom-comp1 v-if="item.type=== 'custom-comp1'">
3.2.2 基于component的动态组件方案

component是vue3内置的特殊组件,一个用于渲染动态组件或元素的“元组件”。接口的定于如下:

interface DynamicComponentProps {
    is: string | Component 
}

查看Vue3官网:component的定义和使用

重构我们渲染器v0.2:


import Input from './Input.vue' 
import Select from './Select.vue' 

const pageCompList = [{
    type: 'input',
    comp: Input
}, {
    type: 'select',
    comp: Select
}]

<div v-for="(item, index) in pageCompList">
  <component :is="item.comp" v-bind="component"></component>
</div>

3.3 Lemon-form 渲染器代码说明
  1. 新增元素为什么选题数组的splice方法:开发的过程中,考虑到后期会支持按下标新增,所以选择了splice方法
const createCompByClick = (item: any) => {
  const createElement = createByClickOrClone(item) // createByClickOrClone 查看源码
  pageCompList.value.splice(pageCompList.value.length, 0, { ...createElement })
}

  1. Lemon-form渲染器的核心源码

渲染器应用

// 渲染器逻辑部分,我们封装了一个 FormComponent 组件之处理动态渲染逻辑
<div v-for="(item, index) in pageCompList" :key="item?.name" :class="{
    'cursor-move': true,
    'form-item': true,
    'active-comp': activeComp.id == item?.id
    }" @click="selectComp(item)">
    <FormComponent :key="item?.id" @compControl="compControl"
       @addItem="addItem($event, item, index)" 
       :component="item" 
       :formConfig="selectForm"
      :type="item?.type" 
      :isDev="isFormEditorDevBool" 
      :selectedComp="getActiveComp()"></FormComponent>
</div>
                  

FormComponent.vue 表单渲染器核心引擎,如果是商业SDK,这部分可以封装起来🐶🐶🐶

// 动态渲染组件爱你
<component 
    :key="currentComp" 
    :isSelected="component?.id === selectedComp?.id" 
    :isDev="isDev"
    :is="getCompConfig(props.type).comp" 
    v-bind="component"></component>
    
// 根据类型,动态创建组件和组件配置  
function getCompConfig(type: any) {
  const compType = { comp: getTypeToComponent(type) }
  const comp = { ...compConfig, ...compType }
  return comp
}

// 根据类型获取渲染组建
function getTypeToComponent(type: string) {
  const compsObject: any = {
    Radio: RadioComponent,
    Input: InputComponent,
    Textarea: TextareaComponent,
    Checkout: CheckoutComponent,
    Date: DateComponent,
    DateRange: DateRangeComponent,
    Time: TimeComponent,
    TimeRange: TimeRangeComponent,
    Url: UrlComponent,
    Number: NumberComponent,
    Switch: SwitchComponent,
    Upload: UploadComponent,
    Divider: DividerComponent,
    Paging: PagingComponent,

    // 评分和满意度
    Rate: RateComponent,
    NPS: NPSComponent,

    // 联系信息
    Name: NameComponent,
    Gender: GenderComponent,
    WX: WXComponent,
    Email: EmailComponent,
    IDCard: IdCardComponent,
    Phone: PhoneComponent,
    TelePhone: TelePhoneComponent,
    Address: AddressComponent,
  }
  const comp = compsObject[type]
  return comp
}

跳转到渲染器阅读源码