引言
- 最近在开发过程中遇到了典型的多层嵌套字段的长表单的问题,为了之后在遇到类似问题时可以快速找到最适合的解决方案,特在此将各种方案的优缺点做一个比较和记录。
- 技术栈:Vue3 + Pinia + Ant Design Vue
- 页面组件关系示意图

// 整个表单中共4层数组嵌套,每一层数组即上图中用外框圈起来的部分
const obj = {
A: 'A',
B: 'B',
C: 'C',
D: [
{
D_A: 'D-A',
D_B: 'C-B',
D_C: 'D-C',
D_D: [
{
D_D_A: 'D-D-A',
D_D_B: 'D-D-B',
D_D_C: [
{
D_D_C_A: 'D-D-C-A',
D_D_C_B: 'D-D-C-B',
D_D_C_C: [
{
D_D_C_C_A: 'D-D-C-C-A',
},
{
D_D_C_C_A: 'D-D-C-C-A',
D_D_C_C_B: 'D-D-C-C-B',
},
],
},
],
},
],
},
],
E: 'E',
}
方案一:拆解v-model:value
- 编码:
- 在store中存储新增或编辑时从接口获取到的数据,为字段编写相应的action方法
- 将v-model:value拆解成:value="param"+@change="handleChange"
- 每个字段在表单项中通过:value传递默认值,通过@change触发store中action更新
- 优点:
- 简单,逻辑清楚。像处理普通表单一样处理各字段,没有额外的技术难度。不会在多人写协作开发时造成理解上的难度,也不会出现不同嵌套层级间互相修改导致的爆栈。
- 缺点:
- 要拆分的字段较多,代码量非常大且重复度高。
- 需要向子级传递数组下标。编写sotre中的action时,依赖各嵌套层级的字段和数组下标,最后的嵌套层级需要自顶向上的3个或4个下标值。
方案二:ref获取子项expose的数据
// Index.vue
<template>
<Basic :ref="basic"></Basic>
<Dispatch :ref="dispatch"></Dispatch>
<button @click="handleSubmit">提交</button>
</template>
<script setup>
const basic = ref(null)
const dispatch = ref(null)
const handleSubmit = ()=>{
const submitData = {}
const basicData = basic.value.getBasicData()
const dispatchData = basic.value.getDisaptchData()
submitData.A = basicData.A
submitData.B = basicData.B
submitData.C = dispatchData.C
submitData.D = dispatchData.D
}
</script>
// Baisc.vue
<template>
<from :model="basic">
<input v-model:value="basic.A"/>
<select v-model:value="basic.B"/>
</form>
</template>
<script setup>
const basic = reactive({
A:'',
B:'',
})
const getBasicData = ()=>{
return basic
}
defineExpose({
getBasicData,
})
</script>
// Dispatch.vue
<template>
<form :model="dispatch">
<input v-model:value="dispatch.C">
<template v-for="(item in dispatch.D">
<Indicator :ref="(el,index)=>{dealRef(el,index)}">
</template>
</form>
</template>
<script setup>
const dispatch = reactive({
C:'',
D:[]
})
// v-for生成的组件list,需要通过函数存储起来,用{}代替[]来避免组件销毁的情况
const refList = ref({})
const dealRef = (el,index)=>{
refList.value[index] = el
}
const getDisaptchData = ()=>{
const dispatchData = {}
dispatch.C = dispatch.C
dispatch.D = Object.keys(refList.value).map((key)=>{
return refList.value[key].getIndicatorData()
})
return dispatchData
}
</script>
<template>
</template>
<script setup>
defineExpose({
getIndicatorData
})
</script>
- 优点
- 不需要传递额外的字段名和数组下标。向每个子组件传递的数据都是与子组件的展示直接相关的,提交按钮点击时函数自上而下调用,数据自下而上返回。与通过触发action直接修改store中的数据相比,省略了很多确定当前字段在store中嵌套的第几层的第几个字段的操作。
- 不依赖store进行状态管理。
- 缺点
- 全部表单都是通过ref模版引用来获取子组件的数据。ref的模版引用其实是vue3提供的获取dom的方案。
方案三:store hooks中存储提交函数
const submitList = []
export const useMyStore = defineStore({
id: 'mystore',
state: {
submitData:{}
},
actions:{
submit(){
submitList.forEach(item=>{
item()
})
await axios({url:'',param:this.submitData,......})
},
updateBasic(param){
Object.assign(this.submitData,param)
},
updateFirstLevel(index1,param){
this.submitData.D[index1] = param
},
updateSecondLevel(index1,index2,param){
this.submitData.D[index1].D_D[index2] = param
},
updateThirdLevel(index1,index2,index3,param){
},
removeSubmit(id){
submitList = submitList.filter((item)=>{
return item.id !== id
})
}
}
export function useMyStoreWithSubmit = (submitFunc)=>{
submitFunc?submitList.push(submitFunc):''
return useMyStore
}
// Index.vue
<template>
<Basic></Basic>
<Dispatch></Dispatch>
<button @click="handleSubmit">提交</button>
</template>
<script setup>
const myStore = useMyStoreWithSubmit()
const handleSubmit = ()=>{
myStore.submit()
}
</script>
// Baisc.vue
<template>
<from :model="basic">
<input v-model:value="basic.A"/>
<select v-model:value="basic.B"/>
</form>
</template>
<script setup>
const basic = reactive({
A:'',
B:'',
})
const submitBaisc = ()=>{
myStore.updateBasic(basic)
}
const myStore = useMyStoreWithSubmit({id:'uniquneId1',func:submitBasic})
onBeforeUnmount(()=>{
myStore.removeSubmit('uniquneId1')
})
</script>
<template>
<form :model="dispatch">
<input v-model:value="dispatch.C">
<template v-for="(item,index) in dispatch.D">
<Indicator :firstIndex="index" :indicator="item"/>
</template>
</form>
</template>
<script setup>
const dispatch = reactive({
C:'',
D:[]
})
const submitDispatch = ()=>{
myStore.updateBasic(dispatch)
}
const myStore = useMyStoreWithSubmit({id:'uniquneId2',func:submitDispatch})
onBeforeUnmount(()=>{
myStore.removeSubmit('uniquneId2')
})
</script>
<template>
<input v-model:value="indicatorData.D_A">
<input v-model:value="indicatorData.D_B">
<template v-for="(item,index) in indicatorData.D_D">
<IndicatorItem :firstIndex="props.firstIndex" :secondIndex="index" :indicatorItem="item"/>
</template>
</template>
<script setup>
const props = defineProps({
firstIndex:{
type:Number
},
indicator:{
type:Object
}
})
const indicatorData = reactive({})
watch(()=>{return props.indicator},()=>{
indicatorData = props.indicator
})
const submitDispatch = ()=>{
myStore.updateFirstLevel(props.firstIndex,indicatorData)
}
const myStore = useMyStoreWithSubmit({id:'uniquneId2',func:submitDispatch})
onBeforeUnmount(()=>{
myStore.removeSubmit('uniquneId2')
})
</script>
- 优点
- 以组件为基本单位提交数据更新。相比于拆分整个表单字段更新数据的方式,编写代码量降低,action数量减少,调用action出错率降低。
- 只在提交按钮点击时触发,无需关注额外的事件。
- 缺点
- 需要额外向子级传递数组下标。
- 组件销毁时需要额外处理store中声明的全局变量。
方案四:$onAction
export const useMyStore = defineStore({
id: 'mystore',
state: {
submitData:{}
},
actions:{
submit(param){
await axios({url:'',param,......})
},
}
export function useMyStoreWithSubmit = (submitFunc)=>{
return useMyStore
}
// Index.vue
<template>
<Basic></Basic>
<Dispatch></Dispatch>
<button @click="handleSubmit">提交</button>
</template>
<script setup>
const myStore = useMyStoreWithSubmit()
const handleSubmit = ()=>{
const param = {}
myStore.submit(param)
}
</script>
// Baisc.vue
<template>
<from :model="basic">
<input v-model:value="basic.A"/>
<select v-model:value="basic.B"/>
</form>
</template>
<script setup>
const basic = reactive({
A:'',
B:'',
})
myStroe.$onAction(({ name, store, args, after, onError })=>{
if(name==='submit'){
args[0].A = basic.A
args[0].B = basic.B
}
})
const myStore = useMyStoreWithSubmit({id:'uniquneId1',func:submitBasic})
</script>
// Dispatch.vue
<template>
<form :model="dispatch">
<input v-model:value="dispatch.C">
<template v-for="(item,index) in dispatch.D">
<Indicator :firstIndex="index" :indicator="item"/>
</template>
</form>
</template>
<script setup>
const dispatch = reactive({
C:'',
D:[]
})
myStroe.$onAction(({ name, store, args, after, onError })=>{
if(name==='submit'){
args[0].C = dispatch.C
args[0].D = dispatch.D
}
})
const myStore = useMyStoreWithSubmit()
</script>
// Indicator.vue
<template>
<input v-model:value="indicatorData.D_A">
<input v-model:value="indicatorData.D_B">
<template v-for="(item,index) in indicatorData.D_D">
<IndicatorItem :firstIndex="props.firstIndex" :secondIndex="index" :indicatorItem="item"/>
</template>
</template>
<script setup>
const props = defineProps({
firstIndex:{
type:Number
},
indicator:{
type:Object
}
})
const indicatorData = reactive({})
watch(()=>{return props.indicator},()=>{
indicatorData = props.indicator
})
myStroe.$onAction(({ name, store, args, after, onError })=>{
if(name==='submit'){
args[0].D[props.firstIndex].D_A = indicatorData.D_A
args[0].D[props.firstIndex].D_B = indicatorData.D_B
args[0].D[props.firstIndex].D_D = indicatorData.D_D
}
})
const myStore = useMyStoreWithSubmit()
</script>
- 优点
- 无需额外处理组件销毁的情况。
- 以组件为基本单位提交数据更新。
- 缺点
- 需要处理数据存在但组件没有初始化时的情况。即数组存在3项,但第2项和第3项可能因为组件的懒加载并没有初始化,那也就不能在$onAction时触发数据更新,则最终提交的param中不存在这两项数据。
- $onAction监听store中任何一个action触发,是一种性能上的浪费。
- 需要额外向子级传递数组下标。
- 补充:不用pinia进行状态管理的,可以使用eventbus来代替$onAction,本质是相同的,都是监听事件的触发,进行后续操作。