包阅导读总结
1. 关键词:动态表单、配置化、联动、运行时、增强
2. 总结:
本文介绍了 B 端联动表单的实现,从构想用户消费方式出发,定义了表单配置的属性和创建函数,对接口函数进行“增强”处理,最后编写运行时将表单对象转为具体组件,文中还给出了代码示例及源码地址,并说明存在不足会持续优化。
3. 主要内容:
– 动态表单的设计思路
– 从用户侧入手构想消费方式
– 用户提供配置,函数处理为表单配置对象,映射为组件
– 定义 `createFormItem` 接收配置,思考其所需属性
– 运行时之前的准备工作
– 对原始数据进行“增强”,包装 `next` 方法使表单项建立联系
– 编写运行时
– 编写组件,通过 `prop` 接收表单配置对象,条件渲染组件,递归消费组件配置对象
– 测试与说明
– 在 `App.vue` 中测试效果,给出源码地址及补充说明
– 指出代码存在不足,会持续优化
思维导图:
文章地址:https://mp.weixin.qq.com/s/MgT2SG4mSSeZZLxpuowSfg
文章来源:mp.weixin.qq.com
作者:keybird
发布时间:2024/7/25 3:01
语言:中文
总字数:2453字
预计阅读时间:10分钟
评分:88分
标签:动态表单,配置化设计,Vue.js,递归组件,前端开发
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
点击关注公众号,“技术干货”及时达!
前言
本文写于“动态表单项目”诞生之初,手把手带写b端联动表单小轮子并记录一下自己的思考,完整复现了作者设计一个“配置化”表单工具时的开发思路与实现技巧
通过(摸鱼学习)本文,将会收获:
最终目标(通过简短明了的声明式配置得到)如下效果:

从用户侧入手——构想用户消费方式
比如要实现一个配置化的联动表单,首先思考用户侧的使用,大致就是用户提供一些配置,中间可能被我们处理成一些标准的联动表单配置对象,最终表单对象映射为具体的联动表单组件。
所以第一步我们可能提供一个函数Fn
去接收用户提供的配置,然后这个函数的作用就是返回一个表单项的配置对象用于映射成组件,所以用户可能的消费方式就是Fn(some options)
,比如我们接收用户配置的方法叫createFormItem
,然后我们就要思考一个联动表单的表单配置需要哪些属性:
-
首先需要一个 type
用来标识要渲染的组件的类型,可能取值为"input" | "select" | "checkbox"
等 -
然后是 payload
参数可能是一个对象,有value
、options
等属性对应目标组件的value
跟option
-
最后从动态表单的实现层面比较核心的就是第三个参数 next
,它是一个函数,决定了下一个表单项是什么,换句话说决定下面渲染什么表单,我们可以提供给用户current
和acients
两个参数,分别代表当前的表单配置项和所有的前置表单项(祖先),这样用户就可以根据前置的所有表单项的取值等自行编写逻辑,从而决定下一个表单项究竟展示什么(「设计核心:下一项表单展示什么由他的所有前置表单的状态决定」)
所以createFormItem
方法可能的类型定义如下:
import{reactive}from"vue";
exporttypeTFormItemType='input'|'select'|'checkbox'
exportinterfaceIFormItem{
type:TFormItemType;
payload:any;
next:(current:IFormItem,acients:IFormItem[])=>IFormItem|null
parent:IFormItem|null;//这里之所以给表单项多加一个parent属性是为了后续方便构造acients数组
}
exportfunctioncreateFormItem(
type:IFormItem['type'],
payload:IFormItem['payload'],
next:IFormItem['next']
):IFormItem{
//...
//returnlike{type,paylolikead,next,parent};
}
上面的思考就是从用户侧考虑我们需要什么,然后下面就是一个可能的用户创建表单配置项的具体操作:
constformItem1=createFormItem(
'input',
{
label:'值为show-select则展示下拉框',
value:'show-select'
},
(current,acients)=>{
//当前表单的value为'show-select'时下一个渲染formItem2
if(current.payload.value==='show-select'){
returnformItem2
}
returnnull;
}
)
运行时之前的准备工作——通过接口函数对原始数据进行“增强”
标题的意思可以粗略的理解为:我们的接口函数接收了用户的原始配置,然后我们可以对这些配置数据,或者说依赖这些数据做一些处理,从而达成一定的目的,为运行时之前做进一步的准备工作。
具体以我们正在实现的动态表单为例,所谓接口函数就是指暴露给用户的接口,这里也就是createFormItem
方法;最终消费表单配置对象的是一个组件,也就是说真正的运行时是组件的渲染,只有在运行时,我们才可以正确的提供next
方法的current
和acients
参数,或者说current
和acients
参数也是仅仅针对运行时的概念,所以说我们执行createFormItem
的时候还不到真正调用用户传入的next
方法的时候,但是我们直接透传原始的next
方法给运行时(组件),我们是没有办法构造acients
的,因为formItem
节点之间是孤立的。「所以我们对原始next
多包装一层再传给运行时,包装一层的目的就是让formItem
之间建立联系,我喜欢称呼这样的操作为“逻辑增强”」 。
createFromItem
的完整逻辑与思路注释如下:
exportfunctioncreateFormItem(
type:IFormItem['type'],
payload:IFormItem['payload'],
next:IFormItem['next']
):IFormItem{
//对next方法进行“增强”,核心逻辑还是调用next,并且返回next的返回值
constnextFunc:IFormItem['next']=(current,acients)=>{
constnextItem=next(current,acients);
//增强:
//调用next方法确定下一个表单项的时候,也就意味着下一个表单项的parent是当前表单项,所以给下一个表单项添加parent属性
//最终目的还是让表单项之间建立关联,从而为运行时提供构造acients的条件
if(!nextItem){
returnnull
}
nextItem.parent=current;
returnnextItem;
}
//最终一定要返回一个响应式对象(状态改变影响视图更新)
returnreactive({
type,
payload,
next:nextFunc,
parent:null,
})
}
万事俱备,成功前的最后一步——依赖前置成果编写运行时
我们的目标是动态表单,自然最后一步是把前面创建的表单对象翻译成具体的组件,而且在组件渲染时,我们还需要计算acients
等然后正儿八经的、真正的去调用next
方法,前面对next
方法的“增强”也终于到了“用武之地”。
至于组件如何编写,自然是通过prop
接收一个表单配置对象,然后条件渲染具体的组件。然后还需要通过next
方法确定下一个组件渲染什么,在html结构部分可以通过「递归的形式消费“链表式”的组件配置对象」。
FormItem.vue
:
<template>
<templatev-if="props.formState">
<el-form-item:label="props.formState.payload.label">
<!--根据props.formState.type进行条件渲染,消费当前配置对象渲染为表单组件-->
<el-inputv-if="props.formState.type==='input'"v-model="props.formState.payload.value"/>
<el-selectv-if="props.formState.type==='select'"v-model="props.formState.payload.value">
<el-option
v-for="iteminprops.formState.payload.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-checkbox-groupv-if="props.formState.type==='checkbox'"v-model="props.formState.payload.value">
<el-checkbox
v-for="iteminprops.formState.payload.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-checkbox-group>
</el-form-item>
<!--递归使用FromItem组件,并调用getNext方法确定下一个组件配置对象-->
<form-item:form-state="getNext()"></form-item>
</template>
</template>
<scriptsetuplang="ts">
import{IFormItem}from'./FormItem';
import{ElFormItem,ElSelect,ElInput,ElCheckboxGroup,ElCheckbox}from'element-plus';
constprops=defineProps<{formState:IFormItem|null}>();
//组件内getNext即为运行时(真正去调用的时机),此时提供current与acients
constgetNext=()=>{
constcurrent=props.formState;
if(!current)returnnull;
//构造acients
letptr=current;
constacients:IFormItem[]=[];
while(ptr&&ptr.parent){
//指针移动
ptr=ptr.parent;
//浅拷贝&插入acients
constacient=ptr;
acients.unshift(acient);
}
returnprops.formState?.next(current,acients);
}
</script>
最后我们可以在App.vue
中测试一下效果,如下也就是我们编写的动态表单的消费方式:
<scriptsetuplang="ts">
importdynamicFormItemfrom'./components/dynamic-form/DynamicForm';
importFormItemfrom'./components/dynamic-form/FormItem.vue';
import{ElForm}from'element-plus';
</script>
<template>
<el-form>
<form-item:form-state="dynamicFormItem"></form-item>
</el-form>
</template>
<stylescoped>
</style>
源码地址 & 补充说明
-
源码已上传git:https://gitee.com/jin-rongda/dynamic-forms -
此文章使用的代码是基于git第一次提交版本,是实现动态表单的最精简版本,但同时存在很多不足,比如配置不够方便,通过 acients
数组访问祖先控件不够方便等问题,上面version的问题可能也已经解决,需要访问如上代码回滚到第一次提交即可。 -
这个小轮子会不断改善,优化用户体验 & 规范代码逻辑等等,欢迎大家cr指正!