网站首页 > 开源技术 正文
作者:离尘不理人
转发链接:https://segmentfault.com/a/1190000022909876
最近在项目开发过程中,有个一个多级多选的公共组件开发需求,特在这里记录下开发过程中所做的一些优化以及分享一下我是如何从零开发并设计一个组件的思路,希望给阅读这篇文章的读者带来一点收获。
效果预览
单个项选中
多个部分项选中
需求分析
在拿到需求之后,我们首先要做的是需求分析;通过上面的效果预览我们可以初步知道我们所需要处理的核心逻辑:
- 默认加载第一层级数据
- 鼠标 hover异步获取数据切换下级渲染数据
- 鼠标点击点击当前项状态改变:选中 or 未选中当前项的父级状态改变:选中、半选、不选中,并且需要递归处理当前项的子级状态改变:全选、全不选
组件设计
在设计组件之前,我们需要考虑组件的性能、通用性等问题;如何设计一个与业务解耦的组件,是我们需要首先考虑的问题;那么,如何将组件数据请求与业务解耦呢:
- 组件提供一个 service 入参,service 是一个返回 Promise 的异步请求方法
- 组件提供一个 dataMapper,用来做数据转换,将 service 请求返回的值转化为符合我们组件数据结构的数据
- 组件内部通过调用外部传入的 service 来获取数据
入参设计如下:
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 操作,我们主要是需要:
- 处理异步数据的获取与缓存
- 处理当前项的子级数据状态;通过在 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 逻辑详情
鼠标 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);
};
Q&A
到这里我们就基本介绍完了如何从 0 到 1完整的设计一个多级多选的组件;该组件支持任意层级的数据,只需要满足我们的层级依赖关系的数据结构,将能复用这个组件
但是我们还有几个思考题:
- 如果多选组件还需要能展示禁选项,逻辑如何调整?
- 如何解耦 DOM 结构与 CSS 实现
这两个问题欢迎各位在下面讨论
推荐JavaScript经典实例学习资料文章
《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》
《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》
《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》
《手把手教你7个有趣的JavaScript 项目-上「附源码」》
《手把手教你7个有趣的JavaScript 项目-下「附源码」》
《JavaScript 使用 mediaDevices API 访问摄像头自拍》
《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》
《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》
《可视化的 js:动态图演示 Promises & Async/Await 的过程》
《Pug 3.0.0正式发布,不再支持 Node.js 6/8》
《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》
《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》
《JavaScript 已进入第三个时代,未来将何去何从?》
《前端上传前预览文件 image、text、json、video、audio「实践」》
《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》
《推荐13个有用的JavaScript数组技巧「值得收藏」》
《36个工作中常用的JavaScript函数片段「值得收藏」》
《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》
《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》
《JavaScript正则深入以及10个非常有意思的正则实战》
《前端开发规范:命名规范、html规范、css规范、js规范》
《100个原生JavaScript代码片段知识点详细汇总【实践】》
《手把手教你深入巩固JavaScript知识体系【思维导图】》
《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》
《身份证号码的正则表达式及验证详解(JavaScript,Regex)》
《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》
《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《面试中教你绕过关于 JavaScript 作用域的 5 个坑》
作者:离尘不理人
转发链接:https://segmentfault.com/a/1190000022909876
猜你喜欢
- 2024-11-17 游戏编程 | THREE.JS实现游戏操作界面
- 2024-11-17 如何处理 Node.js 中出现的未捕获异常?
- 2024-11-17 10个实用的JS技巧「值得收藏」(js快速入门教程)
- 2024-11-17 细品原生JS从初级到高级知识点汇总(四)
- 2024-11-17 奇葩搞怪GIF动图,这样恶搞小姐姐真的好吗?
- 2024-11-17 推荐三款正则可视化工具「JS篇」(正则视维)
- 2024-11-17 3种Javascript图片预加载的方法详解
- 2024-11-17 44道JavaScript送命题(javascript逻辑题)
- 2024-11-17 web前端:原生js全动画企业官网,开机动画、切屏/分屏动画
- 2024-11-17 Node.js 实现抢票小工具&短信通知提醒(下)「干货」
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)