最近在项目开发过程当中,有个一个多级多选的公共组件开发需求,特在这里记录下开发过程当中所作的一些优化以及分享一下我是如何从零开发并设计一个组件的思路,但愿给阅读这篇文章的读者带来一点收获。
在拿到需求以后,咱们首先要作的是需求分析;经过上面的效果预览咱们能够初步知道咱们所须要处理的核心逻辑:前端
鼠标 hover数组
鼠标点击缓存
在设计组件以前,咱们须要考虑组件的性能、通用型等问题;如何设计一个与业务解耦的组件,是咱们须要首先考虑的问题;那么,如何将组件数据请求与业务解耦呢:数据结构
入参设计以下:app
interface Props { ... // 外部传入服务 service: (args: { parentId: string }) => Promise<{ list: SelectorItemType[] }>; dataMapper?: (args: any) => { list: SelectorItemType[] }; /** * 回显数据 * @default [] */ data?: SelectorItemType[]; onSubmit?: SubmitCallback; onCancel?: () => void; } try { const data = await service({ parentId: itemId }); nextColumnList = dataMapper ? dataMapper(data).list : data.list; } catch (error) { Notification.error(error); nextColumnList = []; }
经过上面的 UI 呈现,如今你们应该有个基础的认识,咱们须要作什么样的需求了。异步
咱们在接到一个需求的时候,先不要着急着码代码,更好的方式是先规划咱们的组件方案设计,而且提早思考好各类逻辑分支;
这里给你们看下个人设计初稿,我习惯性的选择脑图来发散本身的思惟:async
经过上图,咱们可以在大脑中有个大概的清晰认识到咱们须要作哪些核心模块的设计与开发,接下来就是规划咱们的核心模块划分:函数
要设计一个高性能多级多选组件,确定离不开咱们的数据优化部分:数据缓存性能
那么若是如何设计才能作到性能最优呢?经过上面的脑图,咱们初步是经过一个 dataCaheMap
来缓存异步拉取回来的数据,这样子咱们在取的时候,时间复杂度就是 O(1) ;既然是有 Map
来缓存数据,那么用什么做为 key
也是咱们缓存的关键;
在这个组件里面,最终我选择的是:列索引+行索引+id 做为缓存 key优化
这样设计的目的是,防止后台出现同时操做增删改类目配置;经过这种方式,能避免由于后台在同步操做到新增长或者删除了某个类目以后,取的缓存数据仍是旧数据,这点是很关键的!
// 数据缓存映射 Map const [dataCacheMap, setDataCacheMap] = useState<{ [x: string]: SelectorItemType[] }>({}); /** * 获取缓存 key * @param itemId selectedItem id * @param itemIndex selectedItem 当前 item 索引 * @param columnIndex 当前 column 索引 */ const getCacheKey = (itemId: string, itemIndex: number, columnIndex: number) => `${itemId}-${itemIndex}-${columnIndex}`; // 取缓存值 async function getItemList() { const cacheKey = getCacheKey(itemId, itemIndex, columnIndex); let nextColumnList = dataCacheMap[cacheKey]; let _selectedValues = { ...selectedValues }; if (!nextColumnList) { setLoading(true); const data = await service({ parentId: itemId }); // dataMapper 用来自定义数据转换 nextColumnList = dataMapper ? dataMapper(data.list) : data.list; } setDataCacheMap((prev) => ({ ...prev, [`${cacheKey}`]: nextColumnList, })); setLoading(false); ... }
若是咱们组件要与业务解耦,那么必需要将数据请求与组件解耦;因此咱们设计组件的是,提供了一个 service
属性做为异步数据请求服务传入;而且经过 TS 来约束 参数与响应体结构,让接口服务返回的数据符合咱们的组件所需的数据结构:单个数据项必须含有 id, parentId, label 三个必须属性,其中 parentId 是咱们处理级联依赖的关键;针对不一样的业务,可能第一级的 parentId 不同,因此咱们也提供了一个 defaultParentId
做为属性供外部传入
若是服务层的数据没法改变,咱们还提供了 dataMapper 回调函数来帮助咱们格式化返回的数据
/** * 单个类目项 */ export interface SelectorItemType { id: string; /** * @default '0' */ parentId: string; /** * 是否可选 * @default true */ disabled?: boolean; /** * 选项文案 * @default '-' */ label: string; /** * 是否半选状态 * @default false */ indeterminate?: boolean; [x: string]: any; } interface Props { ... // 外部传入请求数据服务 service: (args: { parentId: string }) => Promise<{ list: SelectorItemType[] }>; defaultParentId: string; dataMapper?: (args: any) => { list: SelectorItemType[] }; /** * @default [] */ data?: SelectorItemType[]; onSubmit?: SubmitCallback; onCancel?: () => void; }
在有了前面的『数据缓存』、『数据请求』以后,咱们接下来设计渲染所需的数据结构;从交互层面,咱们最容易想到的是二维数组数据结构;经过二维数组的方式,能方便的帮助咱们渲染所需的 UI;
假设咱们的数据是以下数据格式:
// 组件内部数据源 const [source, setSource] = useState<SelectorItemType[][]>([]);
可是由于咱们的交互上面,是有个『部分选中』这个状态存在,可是这个状态与后台类目无关,只是前端展现须要用到的字段,因此咱们须要对接口返回的数据作一个初始化的操做:将数据源项新增一个半选状态 indeterminate
标志位,后续咱们在处理级联状态的时候,须要频繁的改动到这个状态值
categoryList.forEach((item) => { result.push({ ...item, id: item.categoryId, label: item.title, // 半选状态标志位 indeterminate: false, }); }); <div className={styles.selectorItemContainer}> {column.map((item, index) => { return ( <div key={`${item.id}-${columnIndex}`} className={styles.selectorItem} onMouseEnter={() => debouncedHoverCallback(item.id, index, columnIndex)} > <Checkbox value={Boolean(selectedValues[item.id])} disabled={item.disabled} // 判断是否半选 indeterminate={item.indeterminate} className={styles.checkbox} onClick={() => handleItemClick(index, columnIndex)} > <div className={styles.labelText}>{item.label || '-'}</div> </Checkbox> <Icon className={styles.iconRight} type="arrowright" /> </div> ); })} </div>
咱们的组件是『多级多选』无限层级,在组件渲染的时候,如何判断当前 item 项是否选中,依靠的就是咱们的已选数据 state:
// 已选择类目,组件内部维护状态 const [selectedValues, setSelectedValues] = useState<SelectedMap>({}); <Checkbox // 判断是否选中 value={Boolean(selectedValues[item.id])} disabled={item.disabled} indeterminate={item.indeterminate} className={styles.checkbox} onClick={() => handleItemClick(index, columnIndex)} > <div className={styles.labelText}>{item.label || '-'}</div> </Checkbox>
经过打平数据结构,咱们无需关心渲染层级,时间复杂度层面也是保持 O(1);
鼠标 hover 操做,咱们主要是须要:
鼠标 click 操做,核心逻辑:
在咱们选中操做完成以后,咱们须要将用户选择的数据提交给后台,一般多级多选的数据结构设计是平级设计,因此当咱们父级若是是选中的数据,那么它的子级数据就没有必要提交给后台了;
因此咱们须要冲选中池中过滤出父级 parentId 不在选中池中的数据,这个就是咱们最终须要返回给用户与后台的数据
const handleSubmit = () => { const result: SelectorItemType[] = Object.keys(selectedValues).map( (key) => selectedValues[key], ); // 核心逻辑:过滤出当前 parentId 不在选中池中数据,就表示它的父级没有选中 const filterData = result.filter((item) => !selectedValues[item.parentId] || !item.parentId); onSubmit && onSubmit(filterData); };
到这里咱们就基本介绍完了如何从 0 到 1完整的设计一个多级多选的组件;该组件支持任意层级的数据,只须要知足咱们的层级依赖关系的数据结构,将能复用这个组件
可是咱们还有几个思考题:
这两个问题欢迎各位在下面讨论