I 前言
Antd是基于Ant Design设计体系的React UI组件库,主要用于研发企业级中后台产品,在前端很多项目中都有使用。除了提供一些比较基础的例如Button
、Form
、Input
、Modal
、List
…组件,还有Tree
、Upload
、Table
这几个功能集成度比较高的复杂组件,其中Tree
组件的应用场景挺多的,在一些涉及显示树形结构数据的功能中可以体现:目录结构展示、族谱关系图…,总之在需要呈现多个父子层级之间结构关系的场景中就可能用到这种Tree组件,Antd虽然官方提供了Tree组件但是它的功能比较有限,定位是主要负责对数据的展示工作,树数据的增删查改这些功能基本没有支持,但是Antd Tree的属性支持比较完善,我们可以基于Antd树来实现支持编辑功能的EditableTree
组件。
源码:nojsja/EditableTree,克隆整个仓库下来后可以直接运行起来。
已经发布为npm组件,可以直接安装:
1 | $: npm install editable-tree-antd |
预览
II 功能分析
III 实现解析
基于React / Antd / Mobx
文件结构
— index.js – 入口文件,数据初始化、组件生命周期控制、递归调用TreeNode
进行数据渲染
— Tree.js – Tree类用于抽象化树形数据的增删查改操作,相当于Model
层
— lang.js – 多语言文件
— TreeNode.jsx – 单层树节点组件,用于隔离每层节点状态显示和操作
------- TreeNodeDisplay.jsx – 非编辑状态下树数据的展示
------- TreeNodeNormalEditing.jsx – 普通节点处于编辑状态下时
------- TreeNodeYamlEditing.jsx – yaml节点处于编辑状态下时
------- TreeNodeActions.jsx – 该层级树节点的所有功能按钮组
— styles / editable-tree.css – 树样式
— styles / icon-font / * – 图标依赖的iconfont文件
实现原理
- 先来看下Antd原生需要
Tree
数据格式:
1 | [ |
每一层级节点除了需要基本的
title
(文字label)、key
(节点唯一标识)、children
(子结点列表)属性外,还有其它很多自定义参数比如配置节点是否选中等等,这里就不对其它功能配置项做细研究了,感兴趣可以查看官方文档。在官方说明中
title
值其实不只是一个字符串,还可以是一个ReactNode,也就是说Antd官方为我们提供了一个树改造的后门,我们可以用自己的渲染逻辑来替换官方的title
渲染逻辑,所以关键点就是分离这个title
渲染为一个独立的React组件,在这个组件里我们独立管理每一层级的树节点数据展示,同时又向这个组件暴露操作整个树形数据的方法。另一方面Tree型数据一般都需要使用递归逻辑来进行节点渲染和数据增删查改,这里TreeNode.js
就是递归渲染的Component对象,而增删查改逻辑我们把它分离到Tree.js
Model里面进行管理,这样子思路就比较清晰了。
关键点说明:index.js
入口文件,用于:数据初始化、组件生命周期控制、递归调用
TreeNode
进行数据渲染、加载lang文件等等
在生命周期
componentDidMount
中我们初始化一个Tree Model,并设置初始化state数据。在
componentWillReceiveProps
中我们更新这个Model和state以控制界面状态更新,注意使用的Js数据深比较函数deepComparison
用来避免不必要的数据渲染,数据深比较时要使用与树显示相关的节点属性裸数据
(见方法getNudeTreeData
),比如nodeName
,nodeValue
等属性,其它的无关属性比如id
和depth
需要忽略。formatNodeData
主要功能是将我们传入的自定义树数据递归 “翻译” 成Antd Tree渲染需要的原生树数据。
1 | [ |
- 代码逻辑:
1 | ... |
关键点说明:Tree.js
Tree类用于抽象化树形数据的增删查改操作,相当于
Model
层
逻辑不算复杂,很多都是递归树数据修改节点,具体代码不予赘述:
1 | export default class Tree { |
关键点说明:TreeNode.jsx
表示单个树节点的React组件,以下均为其子组件,用于展示各个状态下的树层级
TreeNodeDisplay.jsx – 非编辑状态下树数据的展示
TreeNodeNormalEditing.jsx – 普通节点处于编辑状态下时
TreeNodeYamlEditing.jsx – yaml节点处于编辑状态下时
TreeNodeActions.jsx – 该层级树节点的所有功能按钮组
每个层级节点都可以添加子节点、添加同级节点、编辑节点名、编辑节点值、删除当前节点(一并删除子节点),nameEditable
属性控制节点名是否可编辑,valueEditable
树形控制节点值是否可编辑,nodeDeletable
属性控制节点是否可以删除,默认值都是为true
。
isInEdit
属性表明当前节点是否处于编辑状态,处于编辑状态时显示输入框,否则显示文字,当点击文字时当前节点变成编辑状态。
简单的页面展示组件,具体实现见 源码:TreeNode.jsx
IV 遇到的问题&解决办法
树数据更新渲染导致的节点折叠状态重置
想象我们打开了树的中间某个层级进行节点名编辑,编辑完成后点击提交,树重新渲染刷新,然后之前编辑的节点又重新折叠起来了,我们需要重新打开那个层级看是否编辑成功,这种使用体验无疑是痛苦的。
造成树节点折叠状态重置的原因就是树的重新渲染,且这个折叠状态的控制数据并没有暴露到每个TreeNode上,所以在我们自己实现的TreeNode中无法独立控制树节点的折叠/展开。
查看官方文档,传入树的
expandedKeys
属性可以显式指定整颗树中需要展开的节点,expandedKeys
即需要展开节点的key值数组,为了将每个树节点折叠状态变成受控状态,我们将expandedKeys
存在state或mobx store中,并在树节点折叠状态改变后更新这个值。
1 | ... |
Antd格子布局塌陷
在
TreeNode.jsx
组件中有一个比较严重的问题,如上文提到的EditableTree
的某一层级处于编辑状态时,该层级中的文字展示组件<span>
会变成输入组件<input>
,我发现在编辑模式下Antd的Row/Col
格子布局正常工作,在非编辑模式下由于节点内容从块元素input
变成了内联元素span
,格子布局塌陷了,这种情况下即使声明了Col占用的格子数量,内容依旧使用最小宽度展示,即文字占用的宽度。推测原因是Antd的
Row/Col
格子布局自身的问题,没有深究,这边只是将<span>
元素换成了<div>
元素,并且在样式中声明div
占用的最小宽度min-width
,同时设置max-width
和overflow
避免文字元素超出边界。
V 结语
其实Tree组件已经不止写过一次了,之前基于Semantic UI
写过一次,不过因为Semantic UI
没有Tree的基础实现,所以基本上是完全自己重写的,基本思路其实跟这篇文章写的大致相同,也是递归更新渲染节点,将各个节点的折叠状态放入state进行受控管理,不过这次实现的EditableTree
最主要一点是分离了treeModel
的数据管理逻辑,让界面操作层TreeNode.jsx
、数据管理层Tree.js
和控制层index.jsx
完全分离开来,结构明了,后期即使想扩展功能也未尝不可。又是跟Antd
斗智斗勇的一次😕…