最近,遇到 select
表单下拉选项过多的问题。由于业务场景需要,选项是由接口请求回来,数量取决于线上数据的多少。
借此,顺便记录下目前遇到 select
组件有哪些优化选择。附上一些例子,使用 vue 写的。当然用 react 也是类似的做法,这个和用什么框架关系不大。
为了文章内容不要太长,例子里的共同样式放在了最后。
一、懒加载
懒加载这个词各位应该也很熟悉。在一个古老的例子,瀑布流显示图片就有这个词语了。就是第一屏只显示几条,用户有兴趣往下拉,再继续加载(或请求)新的数据。
以前的话应该是监听 scroll
事件,判断是否要见底了,来补充新数据。后面出了一个厉害的 IntersectionObserver 方法。具体怎么用就不赘述了,总之就是能监听一个 dom 的显隐,来触发回调。
例子可看代码,支持前端自己分批数据或者每次下拉由后端处理分批
<template>
<div ref="lazy-load-select" class="lazy-load-select" :class="{ active: active }">
<a class="display-label" href="javascript:;" @click="active = !active">{{
displayLabel
}}</a>
<div class="list" v-show="active">
<input
type="text"
class="select-input"
@input="search"
v-model="searchText"
/>
<ul class="select-ul" ref="select-ul">
<li
v-for="item in displayData"
ref="select-li"
:class="{ 'select-li': true, selected: item.value === currentValue }"
:key="item.value"
@click="select(item.value)"
>
{{ item.label }}
</li>
</ul>
</div>
</div>
</template>
<script>
function genData({ pageno, count, keyword }) {
return new Array(count).fill('1').map((_, index) => {
const value = (pageno - 1) * count + index + 1 + '';
return {
value,
label: 'LazyLoad-' + value + (keyword || ''),
};
});
}
const originData = genData({ pageno: 1, count: 100 });
// 调试用的声明,true:使用全部数据返回,由前端分批,false:分批从后端获取数据
const defaultProps = false ? {
originData: () => originData,
getData: undefined,
} : {
originData: () => [],
getData: ({ pageno, count, keyword }) => {
return Promise.resolve(genData({ pageno, count, keyword }));
},
};
// 上面是调试示例用的代码
const ONE_LOAD = 10; // 每次加载10条
const LAZY_LOAD_COMP = 2; // 倒数第几个开始新的加载
export default {
name: 'LazyLoadSelect', // 懒加载下拉菜单,支持前端分批处理或后端分批处理
props: {
originData: {
type: Array,
default: defaultProps.originData,
},
getData: {
type: Function,
default: defaultProps.getData,
},
},
data() {
return {
currentValue: 0,
active: false,
searchText: '',
displayData: [],
filterData: [],
intersectionObserver: null,
};
},
computed: {
displayLabel() {
const selectd = this.originData.find(
item => item.value === this.currentValue
);
return selectd ? selectd.label : '请选择';
},
useRemoteData() {
return typeof this.getData === 'function';
},
count() {
return ONE_LOAD;
},
pageno() {
return this.displayData.length / this.count || 1;
},
},
methods: {
async initFilterData(keyword) {
this.displayData = [];
if (this.useRemoteData) {
// 如果有外部方法,初始化则不需要过滤,直接更新接口来的数据即可
const data = await this.loadRemoteData();
this.displayData = data;
} else {
// 如果没有外部方法,初始化则先根据搜索词过滤得出过滤数据,然后再对过滤数据分批
this.filterData = this.originData.filter(item => {
return keyword ? item.label.indexOf(keyword) !== -1 : true;
});
this.displayData = this.filterData.slice(0, 0 + ONE_LOAD);
}
this.$selectUl && (this.$selectUl.scrollTop = 0);
this.updateObserveLayzload();
},
search(event) {
// 应防抖
this.initFilterData(this.searchText);
},
select(value) {
this.currentValue = value;
this.active = false;
this.$emit('change', value);
},
updateObserveLayzload() {
this.$nextTick(() => {
if (this.displayData.length >= this.filterData.length && !this.useRemoteData) {
this.$lazyLoadLi &&
this.intersectionObserver.unobserve(this.$lazyLoadLi);
return;
}
const $liArr = this.$refs['select-li'];
this.$lazyLoadLi &&
this.intersectionObserver.unobserve(this.$lazyLoadLi);
this.$lazyLoadLi = $liArr[$liArr.length - 1 - LAZY_LOAD_COMP];
this.intersectionObserver.observe(this.$lazyLoadLi);
});
},
async loadRemoteData() { // 从外部方法获取数据
try {
const data = await this.getData(
{ pageno: this.pageno, count: this.count, keyword: this.searchText }
);
return data;
} catch(error) {
console.error('get data error:', error);
}
return [];
},
async getNewBatchData() {
if (this.useRemoteData) {
// 数据(包括筛选)完全由后端处理
const addData = await this.loadRemoteData();
return addData;
}
// 由前端对数据分批
const addData = this.filterData.slice(
this.displayData.length,
this.displayData.length + ONE_LOAD,
);
return addData;
},
async updateData() {
const addData = await this.getNewBatchData();
this.displayData.push(...addData);
this.updateObserveLayzload();
},
},
created() {
this.initFilterData('');
},
mounted() {
this.$selectUl = this.$refs['select-ul'];
this.intersectionObserver = new IntersectionObserver(entries => {
if (entries[0].intersectionRatio) {
this.updateData();
}
});
const $main = this.$refs['lazy-load-select'];
window.addEventListener(
'click',
(ev) => {
// 判断是否点击在 select 组件上
const isSelectComp = ev.composedPath().find(dom => $main === dom);
if (!isSelectComp) {
this.active = false;
}
}
);
}
};
</script>
Read More