自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

給 Antd Table 組件編寫縮進(jìn)指引線、子節(jié)點(diǎn)懶加載等功能,如何二次封裝開源組件?

開源
在業(yè)務(wù)需求中,有時(shí)候我們需要基于 antd 之類的組件庫定制很多功能,本文就以我自己遇到的業(yè)務(wù)需求為例,一步步實(shí)現(xiàn)和優(yōu)化一個(gè)樹狀表格組件。

[[384776]]

在業(yè)務(wù)需求中,有時(shí)候我們需要基于 antd 之類的組件庫定制很多功能,本文就以我自己遇到的業(yè)務(wù)需求為例,一步步實(shí)現(xiàn)和優(yōu)化一個(gè)樹狀表格組件,這個(gè)組件會(huì)支持:

  • 每個(gè)層級(jí)縮進(jìn)指示線
  • 遠(yuǎn)程懶加載子節(jié)點(diǎn)
  • 每個(gè)層級(jí)支持分頁

本系列分為兩篇文章,這篇只是講這些業(yè)務(wù)需求如何實(shí)現(xiàn)。

而下一篇,我會(huì)講解怎么給組件也設(shè)計(jì)一套簡(jiǎn)單的插件機(jī)制,來解決代碼耦合,難以維護(hù)的問題。

功能實(shí)現(xiàn)

層級(jí)縮進(jìn)線

antd 的 Table 組件默認(rèn)是沒有提供這個(gè)功能的,它只是支持了樹狀結(jié)構(gòu):

  1. const treeData = [ 
  2.   { 
  3.     function_name: `React Tree Reconciliation`, 
  4.     count: 100, 
  5.     children: [ 
  6.       { 
  7.         function_name: `React Tree Reconciliation2`, 
  8.         count: 100 
  9.       } 
  10.     ] 
  11.   } 

展示效果如下:

antd-table

 

可以看出,在展示大量的函數(shù)堆棧的時(shí)候,沒有縮進(jìn)線就會(huì)很難受了,業(yè)務(wù)方也確實(shí)和我提過這個(gè)需求,可惜之前太忙了,就暫時(shí)放一邊了。😁

參考 VSCode 中的縮進(jìn)線效果,可以發(fā)現(xiàn),縮進(jìn)線是和節(jié)點(diǎn)的層級(jí)緊密相關(guān)的。

vscode

 

比如 src 目錄對(duì)應(yīng)的是第一級(jí),那么它的子級(jí) client 和 node 就只需要在 td 前面繪制一條垂直線,而 node 下的三個(gè)目錄則繪制兩條垂直線。

  1. 第 1 層: | text 
  2. 第 2 層: | | text 
  3. 第 3 層: | | | text 

只需要在自定義渲染單元格元素的時(shí)候,得到以下兩個(gè)信息。

  1. 當(dāng)前節(jié)點(diǎn)的層級(jí)信息。
  2. 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)是否是展開狀態(tài)。

所以思路就是對(duì)數(shù)據(jù)進(jìn)行一次遞歸處理,把層級(jí)寫在節(jié)點(diǎn)上,并且要把父節(jié)點(diǎn)的引用也寫上,之后再通過傳給 Table 的 expandedRowKeys 屬性來維護(hù)表格的展開行數(shù)據(jù)。

這里我是直接改寫了原始數(shù)據(jù),如果需要保證原始數(shù)據(jù)干凈的話,也可以參考 React Fiber 的思路,構(gòu)建一顆替身樹進(jìn)行數(shù)據(jù)寫入,只要保留原始樹節(jié)點(diǎn)的引用即可。

  1. /** 
  2.  * 遞歸樹的通用函數(shù) 
  3.  */ 
  4. const traverseTree = ( 
  5.   treeList, 
  6.   childrenColumnName, 
  7.   callback 
  8. ) => { 
  9.   const traverse = (list, parent = nulllevel = 1) => { 
  10.     list.forEach(treeNode => { 
  11.       callback(treeNode, parent, level); 
  12.       const { [childrenColumnName]: next } = treeNode; 
  13.       if (Array.isArray(next)) { 
  14.         traverse(next, treeNode, level + 1); 
  15.       } 
  16.     }); 
  17.   }; 
  18.   traverse(treeList); 
  19. }; 
  20.  
  21. function rewriteTree({ dataSource }) { 
  22.   traverseTree(dataSource, childrenColumnName, (node, parent, level) => { 
  23.     // 記錄節(jié)點(diǎn)的層級(jí) 
  24.     node[INTERNAL_LEVEL] = level 
  25.     // 記錄節(jié)點(diǎn)的父節(jié)點(diǎn) 
  26.     node[INTERNAL_PARENT] = parent 
  27.   }) 

之后利用 Table 組件提供的 components 屬性,自定義渲染 Cell 組件,也就是 td 元素。

  1. const components = { 
  2.   body: { 
  3.     cell: (cellProps) => ( 
  4.       <TreeTableCell 
  5.         {...props} 
  6.         {...cellProps} 
  7.         expandedRowKeys={expandedRowKeys} 
  8.       /> 
  9.     ) 
  10.   } 

之后,在自定義渲染的 Cell 中,只需要獲取兩個(gè)信息,只需要根據(jù)層級(jí)和父節(jié)點(diǎn)的展開狀態(tài),來決定繪制幾條垂直線即可。

  1. const isParentExpanded = expandedRowKeys.includes( 
  2.   record?.[INTERNAL_PARENT]?.[rowKey] 
  3. // 只有當(dāng)前是展示指引線的列 且父節(jié)點(diǎn)是展開節(jié)點(diǎn) 才會(huì)展示縮進(jìn)指引線 
  4. if (dataIndex !== indentLineDataIndex || !isParentExpanded) { 
  5.   return <td className={className}>{children}</td> 
  6.  
  7. // 只要知道層級(jí) 就知道要在 td 中繪制幾條垂直指引線 舉例來說: 
  8. // 第 2 層: | | text 
  9. // 第 3 層: | | | text 
  10. const level = record[INTERNAL_LEVEL] 
  11.  
  12. const indentLines = renderIndentLines(level

這里的實(shí)現(xiàn)就不再贅述,直接通過絕對(duì)定位畫幾條垂直線,再通過對(duì) level 進(jìn)行循環(huán)時(shí)的下標(biāo) index 決定 left 的偏移值即可。

效果如圖所示:

縮進(jìn)線

 

遠(yuǎn)程懶加載子節(jié)點(diǎn)

這個(gè)需求就需要用比較 hack 的手段實(shí)現(xiàn)了,首先觀察了一下 Table 組件的邏輯,只有在有children 的子節(jié)點(diǎn)上才會(huì)展示「展開更多」的圖標(biāo)。

所以思路就是,和后端約定一個(gè)字段比如 has_next,之后預(yù)處理數(shù)據(jù)的時(shí)候先遍歷這些節(jié)點(diǎn),加上一個(gè)假的占位 children。

之后在點(diǎn)擊展開的時(shí)候,把節(jié)點(diǎn)上的這個(gè)假 children 刪除掉,并且把通過改寫節(jié)點(diǎn)上一個(gè)特殊的 is_loading 字段,在自定義渲染 Icon 的代碼中判斷,并且展示 Loading Icon。

又來到遞歸樹的邏輯中,我們加入這樣的一段代碼:

  1. function rewriteTree({ dataSource }) { 
  2.   traverseTree(dataSource, childrenColumnName, (node, parent, level) => { 
  3.     if (node[hasNextKey]) { 
  4.       // 樹表格組件要求 next 必須是非空數(shù)組才會(huì)渲染「展開按鈕」 
  5.       // 所以這里手動(dòng)添加一個(gè)占位節(jié)點(diǎn)數(shù)組 
  6.       // 后續(xù)在 onExpand 的時(shí)候再加載更多節(jié)點(diǎn) 并且替換這個(gè)數(shù)組 
  7.       node[childrenColumnName] = [generateInternalLoadingNode(rowKey)] 
  8.     } 
  9.   }) 

之后我們要實(shí)現(xiàn)一個(gè) forceUpdate 函數(shù),驅(qū)動(dòng)組件強(qiáng)制渲染:

  1. const [_, forceUpdate] = useReducer((x) => x + 1, 0) 

再來到 onExpand 的邏輯中:

  1. const onExpand = async (expanded, record) => { 
  2.   if (expanded && record[hasNextKey] && onLoadMore) { 
  3.     // 標(biāo)識(shí)節(jié)點(diǎn)的 loading 
  4.     record[INTERNAL_IS_LOADING] = true 
  5.     // 移除用來展示展開箭頭的假 children 
  6.     record[childrenColumnName] = null 
  7.     forceUpdate() 
  8.     const childList = await onLoadMore(record) 
  9.     record[hasNextKey] = false 
  10.     addChildList(record, childList) 
  11.   } 
  12.   onExpandProp?.(expanded, record) 
  13.  
  14. function addChildList(record, childList) { 
  15.   record[childrenColumnName] = childList 
  16.   record[INTERNAL_IS_LOADING] = false 
  17.   rewriteTree({ 
  18.     dataSource: childList, 
  19.     parentNode: record 
  20.   }) 
  21.   forceUpdate() 

這里 onLoadMore 是用戶傳入的獲取更多子節(jié)點(diǎn)的方法,

流程是這樣的:

  1. 節(jié)點(diǎn)展開時(shí),先給節(jié)點(diǎn)寫入一個(gè)正在加載的標(biāo)志,然后把子數(shù)據(jù)重置為空。這樣雖然節(jié)點(diǎn)會(huì)變成展開狀態(tài),但是不會(huì)渲染子節(jié)點(diǎn),然后強(qiáng)制渲染。
  2. 在加載完成后賦值了新的子節(jié)點(diǎn) record[childrenColumnName] = childList 后,我們又通過 forceUpdate 去強(qiáng)制組件重渲染,展示出新的子節(jié)點(diǎn)。

需要注意,我們遞歸樹加入邏輯的所有邏輯都在 rewriteTree 中,所以對(duì)于加入的新的子節(jié)點(diǎn),也需要通過這個(gè)函數(shù)遞歸一遍,加入 level, parent 等信息。

新加入的節(jié)點(diǎn)的 level 需要根據(jù)父節(jié)點(diǎn)的 level 相加得出,不能從 1 開始,否則渲染的縮進(jìn)線就亂掉了,所以這個(gè)函數(shù)需要改寫,加入 parentNode 父節(jié)點(diǎn)參數(shù),遍歷時(shí)寫入的 level 都要加上父節(jié)點(diǎn)已有的 level。

  1. function rewriteTree({ 
  2.   dataSource, 
  3.   // 在動(dòng)態(tài)追加子樹節(jié)點(diǎn)的時(shí)候 需要手動(dòng)傳入 parent 引用 
  4.   parentNode = null 
  5. }) { 
  6.   // 在動(dòng)態(tài)追加子樹節(jié)點(diǎn)的時(shí)候 需要手動(dòng)傳入父節(jié)點(diǎn)的 level 否則 level 會(huì)從 1 開始計(jì)算 
  7.   const startLevel = parentNode?.[INTERNAL_LEVEL] || 0 
  8.  
  9.   traverseTree(dataSource, childrenColumnName, (node, parent, level) => { 
  10.       parent = parent || parentNode; 
  11.       // 記錄節(jié)點(diǎn)的層級(jí) 
  12.       node[INTERNAL_LEVEL] = level + startLevel; 
  13.       // 記錄節(jié)點(diǎn)的父節(jié)點(diǎn) 
  14.       node[INTERNAL_PARENT] = parent; 
  15.  
  16.     if (node[hasNextKey]) { 
  17.       // 樹表格組件要求 next 必須是非空數(shù)組才會(huì)渲染「展開按鈕」 
  18.       // 所以這里手動(dòng)添加一個(gè)占位節(jié)點(diǎn)數(shù)組 
  19.       // 后續(xù)在 onExpand 的時(shí)候再加載更多節(jié)點(diǎn) 并且替換這個(gè)數(shù)組 
  20.       node[childrenColumnName] = [generateInternalLoadingNode(rowKey)] 
  21.     } 
  22.   }) 

自定義渲染 Loading Icon 就很簡(jiǎn)單了:

  1. // 傳入給 Table 組件的 expandIcon 屬性即可 
  2. export const TreeTableExpandIcon = ({ 
  3.   expanded, 
  4.   expandable, 
  5.   onExpand, 
  6.   record 
  7. }) => { 
  8.   if (record[INTERNAL_IS_LOADING]) { 
  9.     return <IconLoading style={iconStyle} /> 
  10.   } 

功能完成,看一下效果:

遠(yuǎn)程懶加載

 

每個(gè)層級(jí)支持分頁

這個(gè)功能和上一個(gè)功能也有點(diǎn)類似,需要在 rewriteTree 的時(shí)候根據(jù)外部傳入的是否開啟分頁的字段,在符合條件的時(shí)候往子節(jié)點(diǎn)數(shù)組的末尾加入一個(gè)占位 Pagination 節(jié)點(diǎn)。

之后在 column 的 render 中改寫這個(gè)節(jié)點(diǎn)的渲染邏輯。

改寫 record:

  1. function rewriteTree({ 
  2.   dataSource, 
  3.   // 在動(dòng)態(tài)追加子樹節(jié)點(diǎn)的時(shí)候 需要手動(dòng)傳入 parent 引用 
  4.   parentNode = null 
  5. }) { 
  6.   // 在動(dòng)態(tài)追加子樹節(jié)點(diǎn)的時(shí)候 需要手動(dòng)傳入父節(jié)點(diǎn)的 level 否則 level 會(huì)從 1 開始計(jì)算 
  7.   const startLevel = parentNode?.[INTERNAL_LEVEL] || 0 
  8.  
  9.   traverseTree(dataSource, childrenColumnName, (node, parent, level) => { 
  10.     // 加載更多邏輯 
  11.     if (node[hasNextKey]) { 
  12.       // 樹表格組件要求 next 必須是非空數(shù)組才會(huì)渲染「展開按鈕」 
  13.       // 所以這里手動(dòng)添加一個(gè)占位節(jié)點(diǎn)數(shù)組 
  14.       // 后續(xù)在 onExpand 的時(shí)候再加載更多節(jié)點(diǎn) 并且替換這個(gè)數(shù)組 
  15.       node[childrenColumnName] = [generateInternalLoadingNode(rowKey)] 
  16.     } 
  17.  
  18.     // 分頁邏輯 
  19.     if (childrenPagination) { 
  20.       const { totalKey } = childrenPagination; 
  21.       const nodeChildren = node[childrenColumnName] || []; 
  22.       const [lastChildNode] = nodeChildren.slice?.(-1); 
  23.       // 渲染分頁器,先加入占位節(jié)點(diǎn) 
  24.       if ( 
  25.         node[totalKey] > nodeChildren?.length && 
  26.         // 防止重復(fù)添加分頁器占位符 
  27.         !isInternalPaginationNode(lastChildNode, rowKey) 
  28.       ) { 
  29.         nodeChildren?.push?.(generateInternalPaginationNode(rowKey)); 
  30.       } 
  31.     } 
  32.   }) 

改寫 columns:

  1. function rewriteColumns() { 
  2.   /** 
  3.    * 根據(jù)占位符 渲染分頁組件 
  4.    */ 
  5.   const rewritePaginationRender = (column) => { 
  6.     column.render = function ColumnRender(text, record) { 
  7.       if ( 
  8.         isInternalPaginationNode(record, rowKey) && 
  9.         dataIndex === indentLineDataIndex 
  10.       ) { 
  11.         return <Pagination /> 
  12.       } 
  13.       return render?.(text, record) ?? text 
  14.     } 
  15.   } 
  16.  
  17.   columns.forEach((column) => { 
  18.     rewritePaginationRender(column
  19.   }) 

來看一下實(shí)現(xiàn)的分頁效果:

 

重構(gòu)和優(yōu)化

隨著編寫功能的增多,邏輯被耦合在 Antd Table 的各個(gè)回調(diào)函數(shù)之中,

  • 指引線的邏輯分散在 rewriteColumns, components中。
  • 分頁的邏輯被分散在 rewriteColumns 和 rewriteTree 中。
  • 加載更多的邏輯被分散在 rewriteTree 和 onExpand 中

至此,組件的代碼行數(shù)也已經(jīng)來到了 300 行,大概看一下代碼的結(jié)構(gòu),已經(jīng)是比較混亂了:

  1. export const TreeTable = (rawProps) => { 
  2.   function rewriteTree() { 
  3.     // 🎈加載更多邏輯 
  4.     // 🔖 分頁邏輯 
  5.   } 
  6.  
  7.   function rewriteColumns() { 
  8.     // 🔖 分頁邏輯 
  9.     // 🏁 縮進(jìn)線邏輯 
  10.   } 
  11.  
  12.   const components = { 
  13.     // 🏁 縮進(jìn)線邏輯 
  14.   } 
  15.  
  16.   const onExpand = async (expanded, record) => { 
  17.     // 🎈 加載更多邏輯 
  18.   } 
  19.  
  20.   return <Table /> 

 

有沒有一種機(jī)制,可以讓代碼按照功能點(diǎn)聚合,而不是散落在各個(gè)函數(shù)中?

  1. // 🔖 分頁邏輯 
  2. const usePaginationPlugin = () => {} 
  3. // 🎈 加載更多邏輯 
  4. const useLazyloadPlugin = () => {} 
  5. // 🏁 縮進(jìn)線邏輯 
  6. const useIndentLinePlugin = () => {} 
  7.  
  8. export const TreeTable = (rawProps) => { 
  9.   usePaginationPlugin() 
  10.  
  11.   useLazyloadPlugin() 
  12.  
  13.   useIndentLinePlugin() 
  14.  
  15.   return <Table /> 

沒錯(cuò),就是很像 VueCompositionAPI 和 React Hook 在邏輯解耦方面所做的改進(jìn),但是在這個(gè)回調(diào)函數(shù)的寫法形態(tài)下,好像不太容易做到?

下一篇文章,我會(huì)聊聊如何利用自己設(shè)計(jì)的插件機(jī)制來優(yōu)化這個(gè)組件的耦合代碼。

記得關(guān)注后加我好友,我會(huì)不定期分享前端知識(shí),行業(yè)信息。2021 陪你一起度過。

 本文轉(zhuǎn)載自微信公眾號(hào)「前端從進(jìn)階到入院」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端從進(jìn)階到入院公眾號(hào)。

 

責(zé)任編輯:武曉燕 來源: 前端從進(jìn)階到入院
相關(guān)推薦

2022-10-17 08:03:47

封裝vue組件

2024-03-20 09:31:00

圖片懶加載性能優(yōu)化React

2017-03-28 10:11:12

Webpack 2React加載

2021-03-04 08:19:29

插件機(jī)制代碼

2021-11-22 10:00:33

鴻蒙HarmonyOS應(yīng)用

2022-05-13 08:46:46

jsoneditorjson編輯器

2023-04-10 08:30:30

json編輯器typescript

2010-01-13 13:53:32

VB.NET組件封裝

2021-09-16 14:22:06

微軟WinUI 2.7InfoBadge

2019-04-24 16:12:59

iOSSiriMacOS

2013-11-12 10:46:04

ChromeChrome32 be

2021-06-08 11:31:11

WineWaylandVulkan

2021-02-04 17:04:22

Python編程語言代碼

2021-04-30 17:35:16

前端開發(fā)技術(shù)熱點(diǎn)

2022-07-06 08:29:12

antdInput 組件

2022-01-25 10:34:37

微軟Edge Cana側(cè)邊欄

2024-03-13 13:39:21

2021-02-05 07:03:17

微軟Edge瀏覽器

2021-12-05 21:05:49

前端JSON API

2020-11-20 10:52:54

Antd表格日程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)