前言
业务开发过程中,有一个设备的下拉选择器中有2w+条数据,而且随着时间的推移,数据量还在增加,导致点击选择器之后页面会卡死
解决方案
方案一:通过远程搜索的方式减少数据量
| 优点 | 缺点 |
|---|---|
| 页面加载流畅,前端不用处理数据 | 频繁发请求与后端交互,数据回显需要单独处理 |
项目是vue3,我封装了一个全局通用组件,因为我们的菜单设计比较“另类”,所以调用后端接口的时机是通过路由监听去实现的
<template>
<el-select
:model-value="props.modelValue"
filterable
remote
clearable
:allow-create="props.allowCreate"
:placeholder="$t('common.pp_enter')"
@change="handleDeviceChange"
:remote-method="remoteMethod"
:loading="remoteLoading">
<el-option
v-for="item in deviceOptions"
:key="item.device_id"
:value="item.device_id"
:label="item.description + '-' + item.device_id"
>
<span style="float: left">{{ item.description }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 13px">{{ item.device_id }}</span>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUnmount, computed } from 'vue'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
import { apiGetDevices } from '~/apis/home'
import { debounce } from 'lodash-es'
const props = defineProps({
modelValue: {
type: String
},
query: {
type: Object,
default: {}
},
allowCreate: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['update:modelValue'])
const $store = useStore()
const route = useRoute()
const project = ref(computed(() => $store.state.project))
const remoteLoading = ref(false)
const deviceOptions = ref([] as any)
const handleDeviceChange = (val: string) => {
emits('update:modelValue', val)
}
/**
* @description: 远程搜索设备
* @param {*} query
* @return {*}
*/
const remoteMethod = (query: string) => {
if (remoteLoading.value) return
searchDeviceList(query)
}
const searchDeviceList = debounce(async (val: string) => {
if (val) {
remoteLoading.value = true
const params = { project: project.value.project, name: val, limit: 30 }
const res = await apiGetDevices(params)
remoteLoading.value = false
deviceOptions.value = res.data
}
}, 500)
/**
* @description: 监听路由,初次进入页面/切换版本
* @return {*}
*/
const stopWatch = watch(
() => route.path,
(newPath, oldPath) => {
if (!oldPath || (newPath.split('/')[2] == oldPath.split('/')[2] && newPath.split('/').length == 3)) {
searchDeviceList(route.query.device_id || '11')
}
},
{ immediate: true }
)
onBeforeUnmount(() => {
stopWatch()
})
defineExpose({ deviceOptions })
</script>
首次进入页面的时候也需要拉取一次列表数据,给用户一种下拉框的感觉,要不然就是输入框的感觉了,通过limit来限制每次后端返回的数据量。这种方案还有一个好处是在进行刷新页面下拉框数据回显的时候,可以通过searchDeviceList(route.query.device_id || '11')来进行回显。对于单选和多选的回显可以在接口中通过分别定义单选模糊查询字段和多选查询字段来进行区分。
方案二:从后端拉取全量数据,前端通过filter-method去控制展示数量
| 优点 | 缺点 |
|---|---|
| 只需要拉取一次数据 | 前端处理数万条数据也会有性能消耗,且数据回显需要额外处理,后端接口返回时间长 |
这种方案比第一种方案灵活,但是越是灵活就越是会写出一些难以后人维护的代码,通用性较差。
const filterMethod = (val: string) => {
deviceOptions.value = deviceOptions.value.filter((item: any) => item.value.indexOf(val) > -1).slice(0, 30)
}
方案三:采用虚拟下拉列表
element plus提供了虚拟化选择器el-select-v2,可以处理数万条数据
| 优点 | 缺点 |
|---|---|
| 可以处理数万条数据不卡顿 | 样式上,下拉面板宽度不能自适应内容宽度,后续数据增加到几十万条之后可能也会卡顿 |
官方示例代码:
<template>
<el-select-v2 v-model="value" filterable :options="options" placeholder="Please select" style="width: 240px" multiple >
<template #default="{ item }">
<span style="margin-right: 8px">{{ item.label }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 13px"> {{ item.value }} </span>
</template>
</el-select-v2>
</template>
总结
考虑到后期数据量还会一直增加,最终采取了第一种方案,不知道还有没有更好的方法,可以提高用户体验