最近在項目開發過程當中,有個一個多級多選的公共組件開發需求,特在這裏記錄下開發過程當中所作的一些優化以及分享一下我是如何從零開發並設計一個組件的思路,但願給閱讀這篇文章的讀者帶來一點收穫。
在拿到需求以後,咱們首先要作的是需求分析;經過上面的效果預覽咱們能夠初步知道咱們所須要處理的核心邏輯:前端
鼠標 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完整的設計一個多級多選的組件;該組件支持任意層級的數據,只須要知足咱們的層級依賴關係的數據結構,將能複用這個組件
可是咱們還有幾個思考題:
這兩個問題歡迎各位在下面討論