前言
最近写表单的时候被测试提了好几个bug
,最后排查发现主要是el-select
引起的。
- 第一种情况是远程搜索,页面初始化的时候默认获取50条下拉,然后远程搜索了一个值,保存后再进详情,由于远程搜索的该值不在默认获取的50条里面,所以导致显示的
code
; - 第二个问题类似,选择了一个值后,然后从字典里面把这个值删了,导致回显的
code
; - 第三个问题 和后台约定存储时下拉的
name
和code
都需要存储,由于el-select
默认只能绑定一个code
,所以导致每次都需要change
然后赋值,比较麻烦; - 第四个问题如果页面有好多个
select
,初始化的时候就问请求n
个接口,导致页面变慢,性能上不友好。
所以这里就对el-select
进行一次二次封装,彻底解决掉这些问题,提升开发效率,这里记录下解决过程和思路。
回显问题
关于这个回显问题,也进行搜索了很久,但是基本上都不符合预期,要么就是初始化拿数据的时候把当前数据push
到下拉的options
里面去,要么就是通过cachedOptions
进行push
,或者直接说让用element-plus
中的虚拟select
(element-ui:那我走?)。
鉴于网上搜索的都不行,就只能自己想了,这个时候发现下拉选项都是通过options
配置的,如果我给他加一个默认的option
,是不是就能解决值无法回显的问题了?对现有代码进行改造。
js
<el-select v-model="form.code" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
<el-option :label="form.name" :value="form.code" v-if="(form.name||form.code)&&(options.length === 0 || !options.find(item => item.value === form.code)"></el-option>
</el-select>
当然这个默认的option
也不是时候都显示的,例如当默认下拉选项中包含这个code
时候,就不需要显示,但是如果每个select
这样写代码复用性太差了,所以这里需要对select
进行一个封装。
封装select
封装之前我们需要考虑以下几个问题:
options
下拉选项是通过props
传递进来的,需要考虑options
的值不为label
和value
的情况。- 需要兼容同时绑定
label
和value
值,不需要再change
赋值; change
有时候想要当前选中的下拉项,而不仅仅是value
值;
基于上面的考虑,我们开始对select
进行封装:
js
# base-select.vue
<template>
<el-select v-model="selectValue" :multiple="multiple" placeholder="请选择" :value-key="fieldNames.value" @change="changeSelect" v-bind="$attrs">
<el-option v-for="item in options" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" />
<el-option v-for="item in defaultOption" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" />
</el-select>
</template>
<script setup>
const props = defineProps({
value: {
type: [String, Array]
},
label: {
type: [String, Array]
},
options: {
type: Array,
default: () => []
},
// 是否change的时候返回当前下拉项值
labelInValue: {
type: Boolean,
default: true
},
// 配置option中的想绑定的label和value字段
fieldNames: {
type: Object,
default: () => ({ label: 'label', value: 'value' })
},
//value是否是当前item
valueObject: {
type: Boolean,
default: false
},
//是否多选
multiple: {
type: Boolean,
default: false
}
});
const emits = defineEmits(['update:value', 'update:label', 'change']);
//这里使用vueuse中的useVModel,也可以不使用,自己写也行。
const selectValue = useVModel(props, 'value', emits, { passive: true, defaultValue: props.value });
//value绑定的值,看是否需要绑定整个字段
const valueCompute = computed(() => {
return (val) => {
return props.valueObject ? val : val[props.fieldNames.value];
};
});
//默认下拉框需要出现的逻辑,之所以这里改成是一个数组,是因为他可能多选。
const defaultOption = computed(() => {
const { multiple, valueObject, label, value } = props;
if(!value) return [];
if (multiple) {
if (valueObject) {
return value.filter((item) => !hasItem(item));
}
const arr = [];
value.forEach((item, index) => {
if (!hasItem(item)) {
arr.push(joinArr(label[index], item));
}
});
return arr;
} else if ((label || value) && !hasItem(value)) {
return [joinArr(label, value)];
}
return [];
});
const hasItem = (value) => {
const val = props.valueObject ? value[props.fieldNames.value] : value;
return props.options && props.options.find((item) => item[props.fieldNames.value] === val);
};
const joinArr = (label,value) => {
const { fieldNames } = props;
return {
[fieldNames.label]: label,
[fieldNames.value]: value
};
};
const changeSelect = (val) => {
const options = props.options;
if (props.valueObject) {
emits('change', val);
return;
}
const currentSelect = options.find(item => item[props.fieldNames.value] === val);
emits('update:label', currentSelect?.[props.fieldNames.label]);
emits('change', currentSelect || {});
};
</script>
这里核心逻辑点 的是这个默认下拉是否出现的场景,由于我们下拉框分为单选、多选value
对象,多选普通value
等情况。所以这里要进行一个区分,至于为什么返回的是一个数组,是因为可能是多选,如果多选里面多个值无法正常回显,这里就需要添加多个。
至于同时绑定label
和value
,其实就是change
的时候update
下label
绑定的值即可。
change
想获取当前下拉项,其实和上面绑定label
一样,我们重写这个change
事件,1.更新label
2.将当前下拉项次emit
出去。
至于el-select
一些其他属性例如clearable
这些,或者el-select
的一些其他事件,这里我们使用了v-bind
,他会自动将没有被props
接受的参数向下传递,所以这里就不需要写其他的属性。在vue3
中v-bind
和v-on
进行了合并,只需要写v-bind
即可。
使用:
js
<template>
//如果是vue2,可以使用label.sync
<base-select v-model:label="form.name" v-model:value="form.code" :options="options" :fieldNames="{label:'fieldName',value:'fieldCode'}" labelInValue/>
</template>
select扩展
其实上面select还可以进一步扩展,这个之所以独立出来,是因人而异,有的人喜欢基础版觉得简单可进一步定制,有人喜欢功能强大pro版,开箱即用,所以这里单独独立出来。例如对select
的下拉,远程搜索进行扩展,扩展成 apiSelect
等。
我们可以给select
再加几个参数:api ,params ,remote ,queryField。
- api :请求
options
的url
地址; - params :请求
url
的其他参数; - remote:是否开启远程搜索;
- queryField :远程搜索的关键字参数,默认为
keyword
。
js
# base-select.vue
....其他代码
<script setup>
....其他代码
onMounted(){
if(props.api){
getOptions()
}
}
//远程搜索也调用这个方法即可
const getOptions=(keyword)=>{
const {api,params,queryField}=props
const requestParams={
[queryField]:keyword,
...params,
}
request(api,{...requestParams}).then(res=>{
})
}
</script>
使用:
js
<template>
//如果是vue2,可以使用label.sync
<base-select v-model:label="form.name" v-model:value="form.code" :fieldNames="{label:'fieldName',value:'fieldCode'}" :api="xxxx" />
</template>
性能优化
由于页面可能有多个select
,初始化的时候都会获取接口,导致页面初始化整体性能不好,所以这里我们可以进行一次优化,只有在focus
的时候,如果这个时候没有初始化,就去请求获取options
的接口。
伪代码:
js
<el-select @focus="focusSelect">
</el-select>
<script setup>
const isInit=ref(false)
const focusSelect=()=>{
if(!isInit.value){
getOptions()
}
}
</scipt>
最后
经过这个select
组件的封装,原本代码中大量的change
事件都没有了,整体代码看起来清晰舒服了,希望该文章对你有帮助。