最近在项目开发过程中,有个一个多级多选的公共组件开发需求,特在这里记录下开发过程中所做的一些优化以及分享一下我是如何从零开发并设计一个组件的思路,希望给阅读这篇文章的读者带来一点收获。
在拿到需求之后,我们首先要做的是需求分析;通过上面的效果预览我们可以初步知道我们所需要处理的核心逻辑:
默认加载第一层级数据
鼠标 hover
鼠标点击
在设计组件之前,我们需要考虑组件的性能、通用型等问题;如何设计一个与业务解耦的组件,是我们需要首先考虑的问题;那么,如何将组件数据请求与业务解耦呢:
入参设计如下:
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 呈现,现在大家应该有个基础的认识,我们需要做什么样的需求了。 我们在接到一个需求的时候,先不要着急着码代码,更好的方式是先规划我们的组件方案设计,并且提前思考好各种逻辑分支; 这里给大家看下我的设计初稿,我习惯性的选择脑图来发散自己的思维:
通过上图,我们能够在大脑中有个大概的清晰认识到我们需要做哪些核心模块的设计与开发,接下来就是规划我们的核心模块划分:
要设计一个高性能多级多选组件,肯定离不开我们的数据优化部分:数据缓存 那么如果如何设计才能做到性能最优呢?通过上面的脑图,我们初步是通过一个 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 操作,我们主要是需要:
注意:在 Hover 事件过程中,我们需要对 debounce 操作
import { useDebouncedCallback } from 'use-debounce';
const [debouncedHoverCallback] = useDebouncedCallback(
(itemId: string, itemIndex: number, columnIndex: number) => {
setQueryData({
itemId,
columnIndex,
itemIndex,
});
},
100,
);
<div
key={`${item.id}-${columnIndex}`}
className={styles.selectorItem}
onMouseEnter={() =>
debouncedHoverCallback(item.id, index, columnIndex)
}
>
....
</div>
鼠标 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完整的设计一个多级多选的组件;该组件支持任意层级的数据,只需要满足我们的层级依赖关系的数据结构,将能复用这个组件 但是我们还有几个思考题:
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。