- 作者:老汪软件技巧
- 发表时间:2024-09-02 17:02
- 浏览量:
一、背景
项目中有一个下图这样的选择组件需要实现,数据是有层级关系的,需要支持单选多选。
看起来很简单,我们可以直接复用 antd 的级联组件,已经支持大部分级联组件需要的功能了。
但是在实际应用中,因为数据请求方式的限制,还是遇到了很多问题,一起看看都是怎么解决的。
二、遇到的问题1. 怎么查数据
级联数据有 10万+ 条,那是一次查所有数据还是一级一级查呢?
项目中有 2 种类型的级联数据,由于上游的限制,类型 1 的级联数据支持一次查所有数据也支持一级一级查,而类型 2 则只支持一级一级查。
而类型 1 的级联数据,必须支持搜索,不然 10万+ 数据没法挨个找。
因此,我们的级联组件要同时支持一次查所有数据和一级一级查这 2 种查数据的模式。
1.1 一次查所有数据
一次查所有就很简单,请求一次接口就行。
1.2 一级一级查数据
一级一级查数据则是先查第一级,这时查到后写死第一级的每一个 children的isLeaf: false,因为只有 isLeaf 为 false 时,才能继续查下一级,直到查不到。
2. 初始化渲染
由于我们的组件要同时支持一次查所有数据和一级一级查这 2 种查数据的模式,导致后面所有的逻辑都要兼容这 2 种数据方式。
初始化渲染,指的是页面详情,组件初始是有选中态的,而这个选中态能在级联组件中渲染出来的条件是,选中了的 id 的数据都已经查到了。
2.1 一次查所有数据
一次查所有的初始化渲染不需要做特别的处理,因为所有数据都有,级联选择器的选中态能正常渲染出来。
2.2 一级一级查数据
一级一级查这种模式,需要根据组件的选中状态,逐步加载数据来渲染选中态。
同时需要处理不同选中模式(单选、多选)的数据请求和渲染逻辑。
3. 级联列表数据渲染
上面已经介绍了,怎么新建时查数据,怎么详情渲染时查数据。
这时我们注意到,组件渲染所需的数据结构和接口返回的数据结构需要互相转换,才能实现正确的数据渲染和选中:
级联列表数据渲染:组件列表的数据结构 接口返回的数据结构级联选中态数据渲染:组件选中态的数据结构 接口返回的数据结构3.1 一次查所有数据
在一次查所有数据的模式下,直接将接口返回的数据结构转换为组件需要的数据结构就行。
3.2 一级一级查数据
一级一级查,根据组件的 value 列表逐级请求数据,并更新组件的树形结构。
3.2.1 获取级联数据
1. 单选
单选时,级联组件的value的数据结构:cat1-cate2-cat3,多级id用-连接。
所以单选就是把 cat1-cate2-cat3转成级联的 id 列表,逐一查询对应级联数据。
2. 多选
多选时,级联组件的value的数据结构:string[][],如[['1', '173', '542']]。需要查 1、173的数据,这样级联组件才能正常渲染选中态。
所以多选就是将所有的倒数第一级之外的节点去重后,逐一查询对应级联数据。
3.2.2 渲染级联数据
有了每一级的级联数据,怎么更新到树结构中呢。
id 根据 level 排序,小的在前先查第一级并更新再查剩下的 id,每查到一个,如果能查到这个 id 的 chidren,就找到 tree 中的这个节点,更新这个节点的 chidren,如果查不到,更新这个节点的 isLeaf 为 true4. 选中态数据渲染4.1 单选4.2 多选5. 单应用有多个级联组件
上面提到,级联数据有 10w+ 条,单应用多个页面存在多个级联组件,我们应该把数据缓存下来,避免每次使用组件都重新加载数据。
5.1 一次查所有数据
一次查所有数据,使用状态管理工具(如 Zustand)存储数据,避免重复请求。
5.2 一级一级查数据
当一个页面用了 5 次级联组件,而且都是多选,每个都选了 5 个数据,每个数据都有 5 层,这时我们通过 id 查对应的级联数据需要请求多少次接口?这其中会不会有 id 重复,如果重复了,数据是不是被覆盖了。
那我们怎么解决这个问题呢?
实现一个RequestCollector 类,用于处理大量请求的防抖和去重。这个类确保了在短时间内对相同的请求进行去重,并按顺序逐个处理。这里的关键点是:
type RequestFn = (e: { catId: string, level: number }) => Promise<void>
interface RequestItem {
requestFn: RequestFn
requestParam: {
catId: string
level: number
}
}
export default class RequestCollector {
private interval: number
private queue: RequestItem[]
private timer: NodeJS.Timeout | null
constructor(interval = 100) {
this.interval = interval
this.queue = []
this.timer = null
}
/**
* 添加请求到队列
* @param {RequestItem} item - 请求项,包含 requestFn 和 requestParam
*/
public addRequest(item: RequestItem): void {
this.queue.push(item)
// 如果计时器不存在,则设置计时器
if (this.timer === null) {
this.timer = setTimeout(() => this.processRequests(), this.interval)
}
}
/**
* 处理队列中的请求
*/
private async processRequests(): Promise<void> {
// 排序并去重
const uniqueSortedRequests = this.sortAndDeduplicate(this.queue)
// 清空队列
this.queue = []
// 逐一处理请求
for (const item of uniqueSortedRequests) {
const { requestFn, requestParam } = item
try {
await requestFn(requestParam)
} catch (error) {
console.error('Request failed:', error)
}
}
// 处理完成后重置计时器
this.timer = null
}
/**
* 根据 requestParam 的 level 对数组进行排序并去重
* @param {RequestItem[]} requests - 包含 { requestFn, requestParam } 的数组
* @returns {RequestItem[]} - 排序并去重后的数组
*/
private sortAndDeduplicate(requests: RequestItem[]): RequestItem[] {
// 排序:先根据 level 升序排序
const sorted = requests
.slice()
.sort((a, b) => a.requestParam.level - b.requestParam.level)
// 去重:保留 level 较小的 requestParam
const unique = sorted.filter(
(item, index, self) =>
index ===
self.findIndex((t) => t.requestParam.catId === item.requestParam.catId)
)
return unique
}
}
可以看到,请求管理器里用到了 setTimeout,setTimeout 里的回调是闭包,因此它是拿不到最新的状态的,所以我们还需要借助一个应用级通用的 Ref 来存储数据。
总结
该组件涵盖了数据获取、渲染、缓存及选中状态管理等多个方面,关键点如下:
通过合理选择数据加载策略、优化初始化渲染流程、准确进行数据结构转换以及高效的请求管理,可以有效地解决复杂的级联选择组件中的各种挑战。实现了在高效的数据处理和优良的用户体验之间的平衡,满足了用户对组件性能和交互的高要求。