前言
最近写表单的时候被测试提了好几个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
,是不是就能解决值无法回显的问题了?对现有代码进行改造。
<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><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><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
进行封装:
# 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是否是当前itemvalueObject: {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># 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># 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
即可。
使用:
<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><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><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
。
# base-select.vue....其他代码<script setup>....其他代码onMounted(){if(props.api){getOptions()}}//远程搜索也调用这个方法即可const getOptions=(keyword)=>{const {api,params,queryField}=propsconst requestParams={[queryField]:keyword,...params,}request(api,{...requestParams}).then(res=>{})}</script># 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># 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>
使用:
<template>//如果是vue2,可以使用label.sync<base-select v-model:label="form.name" v-model:value="form.code" :fieldNames="{label:'fieldName',value:'fieldCode'}" :api="xxxx" /></template><template> //如果是vue2,可以使用label.sync <base-select v-model:label="form.name" v-model:value="form.code" :fieldNames="{label:'fieldName',value:'fieldCode'}" :api="xxxx" /> </template><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
的接口。
伪代码:
<el-select @focus="focusSelect"></el-select><script setup>const isInit=ref(false)const focusSelect=()=>{if(!isInit.value){getOptions()}}</scipt><el-select @focus="focusSelect"> </el-select> <script setup> const isInit=ref(false) const focusSelect=()=>{ if(!isInit.value){ getOptions() } } </scipt><el-select @focus="focusSelect"> </el-select> <script setup> const isInit=ref(false) const focusSelect=()=>{ if(!isInit.value){ getOptions() } } </scipt>
最后
经过这个select
组件的封装,原本代码中大量的change
事件都没有了,整体代码看起来清晰舒服了,希望该文章对你有帮助。