Lemon-Form(柠檬轻表单)是一款类似金数据
和飞书表单
的动态表单系统,只在解决用户可以通过拖拉拽的交互方式创建动态表单。
Lemon-Form表单最初是为了解决客户定制表单页面的开发需求,通过抽象和沉底成一套可复用的动态表单系统,通过无代码的方式,快速搭建出一套成熟的表单功能,最终可以在此系统上完成表单的创建,数据的收集,数据清洗和数据分析。
(一)动态表单实现原理
动态表单的实现原理:基于JsonSchema动态渲染组件,核心在于解耦表单结构配置与组件UI呈现。
- 数据驱动模型:表单页面的UI显示,逻辑交互,数据绑定和表单提交,都是通过配置完成。动态渲染成DOM,而无需直接操作DOM。
- 渲染器:在表单渲染器上,我们采用动态组件
component
的渲染方式,根据表单类型动态渲染不同的配置组件元素。 - 拖拉拽:在交互方式上,我们采用
VueDraggable
拖拉拽完成了组件的创建和顺序的更新,降低表单搭建的使用成本
(二)动态渲染器实现原理
渲染器的介绍
简单来说渲染器是一个函数:根据表单的配置动态渲染出UI的业务逻辑,并动态完成配置的逻辑配置,数据绑定,交互方式。
渲染器是动态表单的核心模块之一,也是类似低代码产品的核心模块,一般渲染渲染器涉及到三个不同的业务场景:
- 开发画布渲染:开发状态画布渲染,涉及到用户动态配置,拖拉拽用户调整导致的动态渲染逻辑,复杂一些的包括多人协同导致的算法调度和实时渲染逻辑,功能最全,最复杂
- 预览:针对开发态的页面进行实时预览,查看页面搭建效果,可以调整预览终端,页面大小,主题等元素并实时获取反馈。
- 线上页面渲染:根据用户请求直接获取页面并渲染,比预览更纯粹的动态渲染过程,针对用户是PC和h5,深背景等页面直出,一般不包括预览选项元素。
其中:针对不复杂的业务场景【预览模块】和【线上页面渲染】是一个功能模块完成。
(三)Lemon-Form 渲染器的实现原理
3.1 创建元素的渲染过程(v0.1)
我们先看一个空表单,创建一个元素的渲染逻辑:
- 首选创建一个用来存储表单元素的数组对象:
pageCompList
- 创建一个元素,就会在
pageCompList
添加一个新的元素 - 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
的渲染方式,我们需要添加很多的动态模版代码,一下场景都无法满足优雅的解决:
- 新增更多的组件,导致模版代码难以维护
// v1 版本
<input v-if="item.type === 'input'"/>
// v2 版本
<input v-if="item.type === 'input'"/>
<select v-if="item.type === 'select'"/>
这里新增了 ...more 100+
- 配置调整导致模版维护成本
// 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+
- 组件升级导致维护成本
// v1 版本
<Input v-if="item.type === 'input'"/>
// v2 版本,input组件,改用了Learn-Form-Input组件
<Learn-Form-Input v-if="item.type === 'input'">
- 动态组件加载不支持:用户希望自己开发的插件可以低成本的集成,目前的方案一定需要调整配置页面,这个是有成本的。
// 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
}
重构我们渲染器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 渲染器代码说明
- 新增元素为什么选题数组的
splice
方法:开发的过程中,考虑到后期会支持按下标新增,所以选择了splice
方法
const createCompByClick = (item: any) => {
const createElement = createByClickOrClone(item) // createByClickOrClone 查看源码
pageCompList.value.splice(pageCompList.value.length, 0, { ...createElement })
}
- 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
}