哈喽,大家好,我是Fine。
今天为大家分享一篇大佬关于在短时间内实现复杂需求的心路历程。过程比较详细,从分析借鉴开源项目,阐述关键步骤,包括拖拽实现、数据转换为组件、定义数据结构、设计组件结构、实现组件,到最后实现路径,关键源码都做了分享。希望大家读完有收获。
以下是正文:
本文用于复盘之前实现的表单生成器需求,记录整体的思路,由于开发时间只有20多天不到一个月,成品比较粗糙,请见谅
上面的视频就是最终完成的效果,还存在一些瑕疵,比如拖拽样式优化、input组件后缀样式优化等。
首先「表单生成器页面」分为上下两个部分:
各自的功能为:
其次为「表单回显页面」,包含数据回填、必填校验、联动显隐、数据提取并保存功能。
当开完需求会的时候,我意识到这个需求的复杂程度不低,此时的我有一点慌🙂;当我询问时间期限的时候,告知我就是这个月底(我是5月6号接到的需求),此时的我感觉慌了🙃;当我再询问这个需求的人员投入的时候,告知我只有我一个前端来处理这个需求,此时的我,已经准备充boss直聘VIP了💀...
开玩笑归开玩笑,该做还得做。其实某些东西是看着大,但很多都是附加功能,只要把核心功能梳理出来,其余的慢慢加就行了。
消化这个需求之后,我暂时没有头绪来处理。我知道这时候我要先去寻找类似的产品取取经。最终我找了Vben开源项目[1],其中有这个模块实现,如下图:
其中的核心功能是一致的,在思索几番之后,我准备先研究一下开源项目这个模块的实现。以下是研究结果:
form
对象,对象中包含控制form
的属性和一个表示表单项
的数组,每一个表单项有唯一ID
,表单项中内容为表单项配置属性form
-> VueDraggable(v-for)
-> DisplayItem
(表单项组件) -> Vue component
(利用is
属性) -> 自定义组件form
组件,form
组件中渲染VueDraggable
组件,VueDraggable
循环渲染自定义的表单项包装组件DisplayItem
,DisplayItem
中渲染Vue component
组件,利用component
的is
属性来渲染自定义组件Vue component
组件的is
属性来渲染自定义组件看似比较大的系统,只要保持耐心,先把主干建立起来,然后逐渐丰富,就能够实现,关键步骤如下:
这里的拖拽,使用vue-draggable-plus
插件来实现,有三种使用方式:
在本次案例中使用的是组件方式,案例中从左侧拖拽到中间面板的行为其实是双列表匹配,官方文档中也有示例,基本使用例子如下:
<template>
<div class="flex">
<VueDraggable
class="flex flex-col gap-2 p-4 w-300px h-300px m-auto bg-gray-500/5 rounded overflow-auto"
v-model="list1"
animation="150"
ghostClass="ghost"
group="people"
@update="onUpdate"
@add="onAdd"
@remove="remove"
>
<div
v-for="item in list1"
:key="item.id"
class="cursor-move h-30 bg-gray-500/5 rounded p-3"
>
{{ item.name }}
</div>
</VueDraggable>
<VueDraggable
class="flex flex-col gap-2 p-4 w-300px h-300px m-auto bg-gray-500/5 rounded overflow-auto"
v-model="list2"
animation="150"
group="people"
ghostClass="ghost"
@update="onUpdate"
@add="onAdd"
@remove="remove"
>
<div
v-for="item in list2"
:key="item.id"
class="cursor-move h-30 bg-gray-500/5 rounded p-3"
>
{{ item.name }}
</div>
</VueDraggable>
</div>
<div class="flex justify-between">
<preview-list :list="list1" />
<preview-list :list="list2" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import { VueDraggable } from 'vue-draggable-plus'
const list1 = ref([
{
name: 'Joao',
id: '1'
},
{
name: 'Jean',
id: '2'
},
{
name: 'Johanna',
id: '3'
},
{
name: 'Juan',
id: '4'
}
])
const list2 = ref(
list1.value.map(item => ({
name: `${item.name}-2`,
id: `${item.id}-2`
}))
)
function onUpdate() {
console.log('update')
}
function onAdd() {
console.log('add')
}
function remove() {
console.log('remove')
}
</script>
如代码所示,只需保证两个列表的group
属性的值一致以及数据结构一致即可;
由于实际需求中,只需要从左侧拖拽到中间,并不需要从中间拖拽回去,所以为了规避这种行为,需要给左侧的group
属性设置为以下内容:
// DraggableGroup 是定义的字符串
// pull 从列表中移动的能力。克隆——复制项目,而不是移动。
// put 是否可以从其他列表中添加元素,或者可以从中获取元素的组名数组。
:group="{ name: DraggableGroup, pull: 'clone', put: false }"
更多插件API
请点击这里[3]
解决了双向拖拽列表之后,此时出现了一个新的问题,左侧的内容是一个树,不是一个扁平的数组,同样要实现拖拽,如下图:
插件也考虑到了这一点,可以实现嵌套的功能,官网嵌套示例[4];逻辑就是组件自调用。
实现了拖拽的需求之后,下一步就是实现从左侧拖拽到中间面板显示为自定义组件。经过上面的步骤之后,就能够知道拖拽本质上是数据的clone
和数组中顺序变化,那么从左侧拖拽过来形成组件,也就是要把这个数据转换成组件。
「数组结构」、「数组每一项代表组件的属性」、「数据转换为组件」,这三个关键词加在一起,很自然就能想到v-for
渲染,由于要实现的组件有五种类型,但是数组的每一项结构是一致的,所以需要一个包装组件,我这里命名为DisplayItem
,再在DisplayItem
中使用component
的is
属性来生成不同类型的组件。图示如下:
由于是数据形成组件,所以要先定义数据结构。到这一步,我先把原型里面的表单的功能和五种组件的功能进行了整理:
结合Vben
中对应的表单结构和表单项结构,定义出了以下结构:
// 当前表单的默认配置
export function createFormConfig(schemas = []) {
return {
// 行内表单模式
inline: true,
// 表单域标签的位置 right/left/top,固定right
"label-position": "right",
// label-width 表单域标签的宽度,例如 '50px'
"label-width": "200px",
// 用于控制该表单内组件的尺寸 medium / small / mini
size: "small",
// 表单配置数组,每一项代表一个组件
schemas,
// 当前选中的控件
currentItem: null
};
}
// 新增模块schema,是上面schemas中的一项
export function addModule(label) {
return {
// 唯一ID,用于绑定值
id: creatUuid(),
// 模块名
label,
// 默认展开
collapse: true,
// 独占一行
row: true,
// 组件类型
componentType: FORM_TYPE_TITLE,
// 组件信息
component: null,
// 配置组件类型
componentConfigType: FORM_CONFIG_TITLE,
// 子项
children: []
};
}
// 新增数据项schema,是上面schemas中的一项
export function addDataItem(label, componentType, parentID) {
return {
// 唯一ID,用于绑定值
id: creatUuid(),
// 模块名
label,
// 默认展开
collapse: true,
// 独占一行
row: componentType === FORM_TYPE_TABLE,
// 组件类型
componentType,
// 组件信息
component: getItemAttr(componentType),
// 配置组件类型
componentConfigType: componentTypeToConfig(componentType),
// 父项
parentID,
// 子项
children: null
};
}
// 展示时的控件
export const FORM_TYPE_DATE = "display-date";
export const FORM_TYPE_RADIO = "display-radio";
export const FORM_TYPE_SELECT = "display-select";
export const FORM_TYPE_INPUT = "display-input";
export const FORM_TYPE_TABLE = "display-table";
export const FORM_TYPE_TITLE = "display-title";
// 配置时的控件
export const FORM_CONFIG_DATE = "config-date";
export const FORM_CONFIG_RADIO = "config-radio";
export const FORM_CONFIG_SELECT = "config-select";
export const FORM_CONFIG_INPUT = "config-input";
export const FORM_CONFIG_TABLE = "config-table";
export const FORM_CONFIG_TITLE = "config-title";
// 五种控件的组件信息,对应上面的component属性
export const ItemAttrs = {
// 日期时间
[FORM_TYPE_DATE]: {
// 组件宽度
width: 200,
// 传给给组件的属性,默认会把所有的props都传递给控件
componentProps: {
placeholder: "请选择日期",
type: "date",
// 显示在输入框的结构
format: "yyyy-MM-dd",
// 最终值的结构
"value-format": "yyyy-MM-dd"
},
// 组件选项
options: [],
// 数据来源
componentData: {
// 对应表
tableName: "",
// 对应的字段
fieldName: ""
},
// 是否隐藏
hidden: false,
// 组件显隐规则
hiddenRules: [],
// 是否必选
required: false,
// 必选提示
message: "数据缺失",
// 组件校验规则
validateRules: []
},
.....剩下4种
}
定义好数据结构之后,先不着急实现组件,因为这个层级和逻辑比较复杂,最好是先把组件结构和组件抽离处理了;结合原型图,一共要处理三类组件:
最终要实现的最外层组件结构如下所示:
<template>
<div class="main-page">
<BaseBox class="box">
<data-item-panel @addSchema="addSchema" />
</BaseBox>
<BaseBox class="box">
<display-panel
:formConfig="formConfig"
:formData="formData"
:IDMap="IDMap"
:currentItem="currentItem"
@setCurrentItem="setCurrentItem"
@deleteCurrentItem="deleteCurrentItem"
/>
</BaseBox>
<BaseBox class="box" full>
<config-panel
:formConfig="formConfig"
:currentItem="currentItem"
@changeFormConfig="changeFormConfig"
@changeSchema="changeSchema"
/>
</BaseBox>
</div>
</template>
......
详细的总体结构图如下:
到这一步就是实现组件,在实现组件的过程中,除了一般的业务处理,有几个点值得注意:
注意点 | 说明 |
---|---|
数据控制 | 整体数据控制在最上层,formConfig(表单内容) 使用prop 传递给子组件,子组件更改操作通知到最上层组件处理,保证数据流向正确和页面实时更新 |
显示当前选中的表单项的匹配的配置组件 | 面板中点击选中表单项时给formConfig 的currentItem 属性赋值,配置面板根据currentItem 中的「配置组件类型」属性,在右侧显示相应类型的配置组件 |
表单数据为空提示 | 配置了required: true 的组件,利用el-form-item 的error 属性提示错误信息 |
显隐规则 | 通过方法将显隐规则数组最终转换为布尔值,结合v-if 实现显示隐藏 |
表单数据回显 | 利用每个组件的唯一id 来达到绑定表单数据的目的 |
表单配置保存校验 | 定义校验函数,收集配置错误信息,点击保存时弹出错误信息表格提示用户 |
在经过上面的步骤之后,「表单生成器页面」完成了,剩下的就是处理表单回显页面和其他页面;表单回显页面其实就是之前写的display-panel
组件,所以剩下的内容比较轻松,就不赘述了。
在这里我梳理了完成这个需求的实际步骤,从零到一,慢慢丰富内容:
完成这个需求,我比较有感触的是,设计一个东西和写业务的区别很大,学到了许多东西,挺有意义的。
原文:https://juejin.cn/post/7383968655077539851
侵删
还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打题全和更新快哦~。
有会员购买、辅导咨询的小伙伴,可以通过下面的二维码,联系我们的小助手。
https://vben.vvbin.cn
[2]https://vue-draggable-plus.pages.dev
[3]https://vue-draggable-plus.pages.dev/api
[4]https://vue-draggable-plus.pages.dev/demo/nested