最近一年多除了工作上日常业务外、主要就是在做 react 组件了。最开始做的 menu / select 都交给别人维护了,随后主要就开始做 tree/tree-select 组件,这里把这两个组件的一些重难点开发心得简要记录下。
说真的,做这些组件以前、自己完全没想到会用那么多时间,直到做了一年多的 tree / tree-select 组件后,回头发现自己平均每周要投入一天时间在这上边时,才真的意识到、就像国外那些一个组件做几年的人并不是能力不够、而是这个要做好真的不容易!
组件开发的整体过程
把 tree 组件的最基本功能:节点展开收起、单选/多选、checkbox 实现出来,大概一周时间就够了、但这其实是理想状态。现实是:
- 如果计算机的算法和数据结构基础不太好,你就得先了解下 tree 的遍历方法 BFS/DFS 和排序算法。
- 如果数据存储方式考虑不周、在大数据量下存取数据效率也不会高。
- 如果对 react 的生命周期和更新机制不够熟悉,你就会碰到大量数据的 多次完全没必要 的重复计算、影响页面整体性能。
所以,是否做出了和是否做好了、是两回事,做好并不容易!做好的另一个重要前提是 单元测试和功能测试 的完善度上,对于 ui 组件、光有单测是远远不够的、功能测试更为重要,而且要尽量覆盖到所有单个功能以及不同功能组合用法场景上,测试的过程、就是捉虫的过程,尽量要让可发现的组件问题全部消灭掉。然后就可以从功能测试代码中提取出较纯粹的 example 来给用户参考使用。这个过程花费的时间与组件的功能点有多少和交互复杂度成正比。
接下来用户在使用过程中、使用环境和用法的差异会很大,这个时候就会出现一些在测试过程中未发现的隐晦问题。这个持续维护的过程周期会比较长、比较琐碎,有了这个过程、用户才会觉得可靠和可信。
在维护过程中、会面对用户的一些扩展功能的需求,比如:异步加载、右键菜单、节点增删改查、节点拖动等,这些扩展功能要做完善、也是真心费时费力的。另外功能变多后、可能之前工作的好好的功能,随着扩展或修改、也很容易变得不好。整体的回归测试成本越来越大,更多使用环境和其他因素带来的问题也变多,更多比较隐晦的bug的发现或不断再生…
组件细节探讨
tree 组件功能可以很多、可以很复杂,但好的一点是功能模块之间还是比较独立的,新增功能基本就是扩展或做成 widget 形式、而比较少需要对原有功能大量修改,能比较容易实现“开闭原则”。但是 tree-select 组件就不是这样了!
一开始很容易会直观的以为 tree-select 不就是 tree + select 组件嘛,两个组件都有、融合一下不是很简单的事儿。事实呢,并非是简单融合,并不简单。先抛出几个要解决的问题:
-
tree 是有层级关系的,但 select 显示的可能只是子节点或父节点,而不是所有 checked 的节点,这就产生了三个场景,即 组件 的
showCheckedStrategy
API 。 -
select 框里选中节点的
x
删除和下拉弹出的树组件节点的uncheck
不是一回事,因为触发元素不同、触发时机不同、只是作用效果类似,但对用户来说需要是透明的,需要在一个函数里返回、并尽量要返回一样的结果(其实不可能完全一样)。另外这个反选或删除操作还要对不同的 showCheckedStrategy 分别做处理。 - tree-select 要有搜索功能,要从大量树节点中筛选并组合出新的子树出来。
- 搜索要解决的一个问题是,多选状态下 select 框里已经选中的节点要保存着而不能被后来再选中的节点覆盖掉。这是一个很自然的需求,但在有 checkable 功能的树节点中处理的复杂度比较大,因为 checkbox 有父子关联的选中关系计算和相应的不同显示效果。
- 另外就是要对 react 数据状态变化的触发源要很敏感,特别是一些叠加或连续触发的状态变化,尽量避免把不同状态的变化混淆到一起做处理(哪怕处理函数很相似)、这样很容易出现问题。
- 例如,因为树的节点数据量比较大,为了性能,需要缓存一些计算后的值避免多次重复计算,如 tree 组件中的 treeNodesStates 即为缓存。
- 而 tree-select 初始化时需要在选择框展示初始选中的节点、这些节点在 checkable 状态下,需要计算节点的父子兄弟选中关系,另外还要区分不同的 showCheckedStrategy 。所以 tree-select 需要预先计算出 treeNodesStates 传递给 tree 组件,这样 tree 组件内部便不需要再重复计算了。
- 但注意:这时候 tree-select 和 tree 就都会修改 treeNodesStates , tree-select 在搜索时、要根据搜索筛选后的数据产生新的 tree,同时就需要计算出一个新的 treeNodesStates 传给 tree 组件,但我之前犯的一个错误就是使用了旧值,这个旧值是在点击 check 树节点时、在 tree 组件内部更新计算出的 treeNodesStates ,而此时 tree-select 搜索后生成的新的 tree 节点和这个旧值是不对应的。
其他的细节问题还有很多、这里也没必要一一列举,想知道就在代码里找吧~
感悟总结
因为功能很多、代码也有很多可复用的地方,所以一个深的体会是:函数式编程中的无副作用的纯函数做法是很有好处的。特别是在代码重构过程中,很自然地会不断把原来有外部状态耦合的函数、修改为纯函数。
一般的做法都认为、函数应该遵循“原子性”,但这样就需要写非常多的函数、结果是时常会感觉难以维护、调用链路很长、找起来很累。个人的一点看法是:函数不需要太原子化、当然也不要过大。
另外一个常见的问题是,因为功能复杂、所以分支和条件判断过多。目前代码里比如 if 语句的条件有不少地方都写的很长,看起来很不优雅。再扩展开来就是代码组织方式上的优化,还是需要做很多思考和完善的。
这两个组件从功能完善度到具体的代码实现方式上等等、还有非常多需要优化的地方,如果以后仍继续负责维护、希望能抽出时间把它做的更好!