对于一个树形数据的要点是
- 主键id
- 父节点id
- 子节点集合children[]
也就是自己的唯一标识符,父节点的唯一标识符,以及子节点信息,承上启下,加自己。当然还有扩展信息,比如深度,权重等。
前端解决方案
思路
假设原始数据为这种select * from tree_table_name类似的查询结果集。即只有单独节点数据信息的数组,不含有子节点集合children[](后端经常返回原始数据不是一两天了)。我们需要通过递归的方式遍历所有节点信息来组装树形数据。这次解决方法对于递归方法参数已经有两个为必定有的:待处理的数据,已处理的数据
对于递归的要点
- 参数:待处理数据,处理后数据结果集,传递参数。
- 返回值
- 递归完成条件:避免死循环,注意何时跳出。
以下先放代码后面再讲讲思路
前端代码
export class TreeConfig{
id: string = 'id';
parentId: string = 'pid';
children: string = 'children';
}
export function handleTree(data: Array<any>, cfg?: TreeConfig) {
let res:Array<any> = [];
if(cfg===undefined){
cfg = new TreeConfig();
}
loadTree(data, res,cfg,undefined);
return res;
}
function loadTree(data: Array<any>, res:Array<any>, cfg: TreeConfig, cur: any){
for (let index = 0; index < data.length; index++) {
const element = data[index];
if(cur === undefined ){
if(element[cfg.parentId] === null || element[cfg.parentId] === ""){//寻找根节点
res.push(element)
data.splice(index,1);
index--;
loadTree(data,res,cfg,element);
}
}else if(element[cfg.parentId] === cur[cfg.id]){//处理非根节点
if(cur[cfg.children] === undefined){
cur[cfg.children] = [];
}
cur[cfg.children].push(element);
data.splice(index,1);
index--;
loadTree(data,res,cfg,element);
}
}
}
先定义TreeConfig是因为我们实际开发中常常遇到的树形数据中的key值可能千奇百怪,但是意思一样,所以给树形结构的数据的key做一个映射。(好比子节点集合不一定就是叫children,还有可能是subs,trees等等)。递归装载数据方法为loadTree,其中参数data为待处理的原始数据(会剔除处理后数据,如果想保留原始数据记得调用前先深度克隆一份),res为处理后的数据,cfg为树形结构的数据的key映射参数,cur为当前处理数据的父节点数据,由于存在根节点,那么对于根节点开始,则cur暂为空undefined。那么这时候就分为两种情况,情况一:当cur为undefined与情况二:cur不为undefined(为一个确切的父节点数据)。
对于情况一:我们需要筛选出根节点数据。原始数据中,成员的父节点的唯一标识为空的为根节点数据(根据实际业务来判断,可能有的业务中根节点的parentId为某个其他值)。将数据存待处理数据源中剔除,并存入res中。然后去递归调用(查找当前节点下的子节点,若没有则不继续递归,若有则会在情况二下继续查找)。index--;的原因是因为这个数据处理后就从待处理就剔除了,需要排除掉。
对于情况二:我们需要找到这个当前父节点下的子节点。即当前处理的数据的parentId等于cur的id。然后将找到的子节点放到他们的父节点的children中,然后继续剔除数据。继续递归调用找这个节点的子节点(如果子节点仍然包含子节点则会情况二继续调用,直到不包含子节点为止,然后执行完函数体返回,即不存在死循环情况)。
java后端解决方案
按照前面思路后端也是可以写类似递归方法的,但是不用这么麻烦,对于经常有各种成熟解决方案的后端,有更好用的工具,而不是去重复造轮子。推荐使用hutool这个工具包(这个工具包包含很多实用工具,其中分页插件也很不错)。maven引入
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
后端代码
List<TestModel> list = testDao.queryList(testModel);
//build 第二个参数null是我这里的数据是所有根节点的parentId为null
return TreeUtil.build(list, null,(treeNode, tree) -> {
//这里的 treeNode 为list中成员(此时为TestModel),tree为返回list中成员
tree.setId(treeNode.getId());
tree.setParentId(treeNode.getPid());
//以上 tree 的id parentId为必设,不然树形数据组装不了
tree.setName(treeNode.getName());
//设置 tree 的扩展属性
tree.putExtra("description", treeNode.getDescription());
tree.putExtra("icon", treeNode.getIcon());
});
当然这个TreeUtil中重载了很多个build方法。这只是其中一个。在这个build方法第三个参数中我们可以灵活操作数据:一个是可以灵活控制树的属性,以及值。比如查询数据需要计算到一个key作为树的属性等,亦或是根据字典转换等等。一个是可以灵活映射id,parentId等。这个 build 方法返回是List<Tree<E>>l类型,即都是树对象Tree的集合,Tree中对于主键来说这里必定是id,对于父节点主键必定为parentId,万一有时候数据返回前端需要parentId为pid,或者其他呢。怎么办?只有在tree的扩展属性中再设置一下例如 tree.putExtra("pid", treeNode.getPid());
吐槽
对于数据处理数据无论是后端还是前端都可以做的时候,经常出现前端后端甩来甩去等现象。可能就几行代码的问题就是推来推去。有时候人与人的想处,比搞几个LeetCode困难算法题还难。