背景与意义

目前参与的项目使用ant-design作为UI组件库,因之前没有使用过,所以想着熟悉一下组件及其相关属性。
当看到Tree组件的showline效果时,想着肯定是通过svg来实现的,于是检查了这个元素,发现是通过伪元素::before和::after实现的,这个过程略显复杂,而且不易理解。于是又去看了element-ui希望找到更加简便的方法,结果发现并不支持该功能。最后又去我们团队自研的组件库去看了看发现也都没有支持。
因此本文通过svg技术为Tree组件的showline功能提供另外一种易于实现且通俗易懂的解决方案,为团队后续实现该功能时可以多一种选择。

svg基础知识回顾

svg基本结构

图像的描述信息 {/ 图形绘制区域 /}

svg坐标定位

tree组件 showline优化 - 图1
以页面左上角(0,0)点为坐标原点,x轴向右为正方向,y轴向下为正方向。

svg基本形状

线段: 矩形: 圆角矩形: 圆形: 椭圆形: 多边形: 折线: 路径:

svg基本属性

fill: 填充 stroke: 描边 stroke-width: 描边宽度 transform: 基础变形

svg实现showline功能具体过程

Tree组件的使用方法

treeData的数据结构

const treeData = [ { title: ‘parent 1’, key: ‘0-0’, children: [ { title: ‘parent 1-0’, key: ‘0-0-0’, children: [ { title: ‘leaf1-0-1’, key: ‘0-0-0-0’, }, { title: ‘leaf1-0-2’, key: ‘0-0-0-1’, }, { title: ‘leaf1-0-3’, key: ‘0-0-0-2’, }, ], }, { title: ‘parent 1-1’, key: ‘0-0-1’, children: […] }, … ] }, … ]

treeData的处理

为每个节点添加level和deep属性

const reduceTreeData = function(data, currLevel = 0) { if (!(data instanceof Array)) throw new TypeError(‘The data should be an array!’); let levelTree = []; for (let k = 0; k < data.length; k++) { let temp = data[k]; let newNode = { …temp, level: currLevel + 1, children: null, isLeaf: false, open: true, }; delete newNode.children; if (temp.children && temp.children.length > 0) { // 计算tree每个子节点的层级level currLevel++ newNode.children = reduceTreeData(temp.children, currLevel); currLevel— newNode.deep = getLeafCount(temp, ‘children’) + 1 newNode.backupChild = newNode.children; } levelTree.push(newNode); } const result = setLeafAttr(levelTree) return result; }

标记出叶子节点

const setLeafAttr = (treeData) => { const setLeaf = (data) => { data.map(item => { if(!item.children || !item.children.length) { let parentNode = findParentNode(treeData, item); if(parentNode && parentNode.children && parentNode.children.length > 1) { item.isLeaf = true if(parentNode.children[parentNode.children.length-1].key === item.key) { item.isLast = true } } } if(item?.children?.length) { setLeaf(item.children); } return item; }) } setLeaf(treeData); return treeData; }

缩进处理

通过绘制react占位达到缩进的效果 width计算: 基本宽度单位为16px 非叶子节点: level 16px 叶子节点: (level + 3) 16
tree组件 showline优化 - 图2

showline处理

非叶子节点的showline处理

同一层级最后一个节点不需要画线,通过isSameLevelLastNode方法判断 { !obj.isLeaf && obj.deep && !isSameLevelLastNode(data, obj) ? ( ) : null }
tree组件 showline优化 - 图3

叶子节点的showline处理

{ obj.isLeaf ? ( <> { obj.isLast ? : } </> ) : null }
tree组件 showline优化 - 图4

缩起处理

将当前节点的孩子节点置空并备份。 更新当前节点的所有祖先节点的deep。 const handlePlus = (e, obj) => { let res = null; res = updateNode(data, obj, { open: false, children: [], deep: 1 }) if(obj.level !== 1) { const parentKey = obj.key.slice(0, obj.key.length -2); let parentNodeList = findAllParentNode(data, parentKey, ‘key’); if(parentNodeList?.length) { res = updateSomeNode(data,parentNodeList); } } setData([…res]); }

展开处理

恢复当前节点的孩子节点。 更新当前节点的所有祖先节点的deep。 const handleMinus = (e, obj) => { const children = (obj.backupChild.length && !obj.children.length) ? obj.backupChild : []; let deep = getLeafCount(obj, ‘backupChild’) + 1; obj && obj.backupChild && obj.backupChild.forEach(child => { if(!child.open && child.backupChild && child.backupChild.length) { deep = deep - child.backupChild.length } }) let res = updateNode(data, obj, { open: true, children: children, deep: deep }) const parentKey = obj.level === 1 ? obj.key: obj.key.slice(0, obj.key.length -2); let parentNodeList = findAllParentNode(res, parentKey, ‘key’); if(parentNodeList?.length) { res = updateSomeNode(res,parentNodeList); } setData([…res]); }

总结

本文运用svg技术中的line和polyLine实现Tree组件的showline功能。整体来说思路简单明了、易于实现。整体流程如下:

  1. Tree组件内部对用户传入的treeData进行处理,添加level、deep和标记叶子节点
  2. level + rect完成缩进功能
  3. deep + line完成非叶子节点的showline功能
  4. isLeaf + polyline完成叶子节点的showline功能

    源码地址

    https://github.com/xiaohong-guo/Tree-Componnet

    参考链接

    https://juejin.cn/post/6844904198060900359
    https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Basic_Shapes
    https://flaviocopes.com/svg/