JQuery-zTree.js使用范例

实现Tree树的插件很多,比如常见的UI:Layui、ElementUI、iView ... 。这里我们介绍一个小巧的构建Tree树的插件 zTree.js

zTree.js 官网API介绍的灰常详细了,这里我们实战使用zTree.js构建一棵Tree树。

写在前面
下列文章中讲述的实例,需要使用的后端数据是已经查询好的,这里我们不讲怎么查询数据,只讲如何使用现有的数据构建Tree树,详细的教程请查看我的 GitHub, 如果你觉得写得好,欢迎star呀!!

起步

使用zTree.js首先需要导入zTree的依赖库文件,传送门。

由于我使用了基于boostrap主题的zTree,所以还是建议大家去我的GitHub项目地址下载(CSS是修改过的),传送门:GitHub

页面中需要引入如下依赖库文件:

<link rel="stylesheet" href="static/lib/bootstrap.min.css"/>
<link rel="stylesheet" href="static/lib/css/demo.css"/>
<link rel="stylesheet" href="static/lib/css/metroStyle/metroStyle.css"/>

<script type="text/javascript" src="static/lib/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="static/lib/jquery.ztree.core.min.js"></script>
<script type="text/javascript" src="static/lib/jquery.ztree.excheck.min.js"></script>
前端构建一棵tree树

查阅zTree.js官网API,构建一棵Tree树很简单:

一、前端初始化一个div,用来展示Tree树

zTree构建的Tree树是用iframe嵌套的,所以不用担心宽度、高度的问题


1 <div class="zTreeDemoBackground">
2   <ul id="tree" class="ztree"></ul>
3 </div>

初始化的div只需要关注id属性即可,因为JS中会根据这个ID找到构建Tree树的位置。

二、javaScript加载Tree树

为了真实点构建Tree树,我这里用一个json文件来模拟请求后端的数据。在同级目录下创建data.json,在其中写入指定格式的JSON字符串:

 1 [{  "id": 21,
 2     "name": "总经理",
 3     "pid": 0,
 4     "parent": true
 5 }, {"id": 26,
 6     "name": "技术部",
 7     "pid": 0,
 8     "parent": true
 9 }, {
10     "id": 27,
11     "name": "项目经理",
12     "pid": 26,
13     "parent": false
14 }, {"id": 28,
15     "name": "项目组组长",
16     "pid": 26,
17     "parent": false
18 }, {
19     "id": 29,
20     "name": "安全部",
21     "pid": 0,
22     "parent": true
23 }, {"id": 30,
24     "name": "网络安全部负责人",
25     "pid": 29,
26     "parent": false
27 }, {
28     "id": 31,
29     "name": "项目安全测试员",
30     "pid": 29,
31     "parent": false
32 }]

然后,写JavaScript代码:

 1 var setting = {
 2     view: {
 3         selectedMulti: true
 4     },
 5     check: {
 6         enable: true,
 7     },
 8     data: {
 9         simpleData: {
10             enable: true,//是否采用简单数据模式
11             idKey: "id",//树节点ID名称
12             pIdKey: "pid",//父节点ID名称
13             rootPId: -1,//根节点ID
14         }
15     }
16 };
17 $(function () {
18   //加载后端构建的ZTree树(节点的数据格式已在后端格式化好了)
19   $.ajax({
20       url: 'data.json',
21       type: 'get',
22       dataType: "json",
23       success: (data) => {
24           console.log(data);
25           $.fn.zTree.init($("#tree"), setting, data);//初始化树节点时,添加同步获取的数据
26       },
27       error: (data) => {
28           alert(data.message);
29       }
30   });
31 });

解释

  1. setting中包含了ztree的所有配置。
  2. view中包含了Tree树的一些视图样式配置,例如是否显示节点间的连线,是否显示节点的图标,等...
  3. selectedMultiview的一个配置参数,设置是否允许同时选中多个节点。
  4. data中包含了要展示的数据以及展示数据的配置,因为我们采用了ajax请求数据,这里需要配置simpleData
  5. simpleData数据展示的配置:enable是否采用简单的数据模式;idKey树节点ID名称;pIdKey父节点ID名称;rootPId根节点ID

以上参数配置,大家最好去参看zTree.js官网API。

如果配置好了setting,那下面就要ajax请求数据并渲染出来。如上在ajax的success回调函数中使用$.fn.zTree.init($("#treeID"),setting,data)渲染树节点,其中第一个参数:树要渲染的位置、第二个参数:刚才写的setting配置,第三个参数:要加载的数据。

如上,我们先看下效果:


JQuery-zTree.js使用范例_数据

其中我们最该关心的是如何实现节点的渲染,说白了就是要弄明白怎样的数据结构zTree才能渲染出一棵树。

Tree树数据结构分析

首先,zTree渲染节点需要的数据一定是JSON格式的数据,且JSON数据的格式和simpleData配置参数有关;想要使用ajax这种方式渲染节点,你必须开启enable: true,其次idKey是树节点ID名称,也就是说树的每个节点都有一个id,我们在这里要指定被渲染的数据中展示id的名称;其次要指定pIdKey,因为你的节点不会都是平级的没有子节点,当需要子节点,就必须指定一个区分父子节点的ID名称;最后就是rootPId表示根节点ID,即最上层的节点ID,一般写为-1即可。

此时,你或许应该参考一下我这篇文章:Shiro实现权限管理之表结构设计 ,表结构的设计和tree树的构建也算是有一部分的关系吧。

如果你的simpleData是这样配置的:

1 simpleData: {
2     enable: true,//是否采用简单数据模式
3     idKey: "id",//树节点ID名称
4     pIdKey: "pid",//父节点ID名称
5     rootPId: -1,//根节点ID
6 }

那么你就应该提供这样的JSON数据:

[{"id": "xx", "pid": "xx", "pid": "xx"},{"id": "xx",....},{....}]

只要名称和JSON数据中对应就行,不然无法渲染出节点。

实例

如何实现默认选中

实现默认选中,就是在初始化树的时候,将(用户)已拥有的节点选项选中。要知道所有的节点数据应该是从数据库中读取出出来的,例如这篇博文 权限管理系统数据库表设计 中用户都可能拥有一个角色,那么在遍历角色树的时候就应该默认选中一些节点表述用户已经拥有了这个节点角色。

  • 如何实现默认选中?

简单一句话:遍历需要默认选中的节点数据(ID..),调用zTree.js相关的方法根据(ID)实现默认选中。

首先我们需要了解:

函数 用处
$.fn.zTree.getZTreeObj('') 获取zTree对象,根据div中指定的ID获取此渲染的ZTree对象,下面的方法都用到此对象调用
zTree.selectNode(treeNode,addFlag,isSilent) 根据上面获取的zTree对象调用selectNode,参数一:要选中的节点数据;参数二:是否允许同时选中多个节点;参数三:为false选中节点自动滚动到可视区域,实现选中子节点的父节点默认展开
zTree.checkNode(treeNode,checked,checkTypeFlag,callbackFlag) selectNode只实现选择了节点,checkNode实现勾选节点,参数二:是否勾选节点;参数三:勾选父节点是否联动勾选其下的子节点;参数四:是否自动触发beforeCheck & onCkeck 回调函数
zTree.getNodeByParam(key,value,parentNode) 获取完全匹配节点数据的JSON对象,参数一:要精确匹配的属性名称;参数二:要精确匹配的属性值;参数三:在某个父节点下查找

用法


 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6     <link rel="stylesheet" href="static/lib/bootstrap.min.css">
 7     <link rel="stylesheet" href="static/lib/css/metroStyle/metroStyle.css">
 8     <link rel="stylesheet" href="static/lib/css/demo.css">
 9 </head>
10 <body>
11 <div class="zTreeDemoBackground">
12     <ul id="tree" class="ztree"></ul>
13 </div>
14 </body>
15 <script type="text/javascript" src="static/lib/jquery-3.3.1.min.js"></script>
16 <script type="text/javascript" src="static/lib/jquery.ztree.core.min.js"></script>
17 <script type="text/javascript" src="static/lib/jquery.ztree.excheck.min.js"></script>
18 <script type="text/javascript">
19     var setting = {
20         view: {
21             selectedMulti: false
22         },
23         check: {
24             enable: true,
25         },
26         data: {
27             simpleData: {
28                 enable: true,//是否采用简单数据模式
29                 idKey: "id",//树节点ID名称
30                 pIdKey: "pid",//父节点ID名称
31                 rootPId: -1,//根节点ID
32             }
33         }
34     };
35     $(function () {
36         //加载后端构建的ZTree树(节点的数据格式已在后端格式化好了)
37         $.ajax({
38             url: 'data.json',
39             type: 'get',
40             dataType: "json",
41             success: (data) => {
42                 console.log(data);
43                 $.fn.zTree.init($("#tree"), setting, data);//初始化树节点时,添加同步获取的数据
44                 checkNodes();
45             },
46             error: (data) => {
47                 alert(data.message);
48             }
49         });
50     });
51 
52     //处理默认选中的方法
53     function checkNodes(){
54         //模拟数据库中已存在的数据(要实现默认选中的数据)
55         var data = [{"id": 21, "name": "总经理", "pid": 0},{"id":'27', "name": "项目经理", "pid": 26}];
56 
57         var zTree = $.fn.zTree.getZTreeObj("tree"); //获取zTree对象
58         data.forEach(row => {
59             zTree.selectNode(zTree.getNodeByParam("id", row.id), true, false);
60             zTree.checkNode(zTree.getNodeByParam("id", row.id), true, false);
61         });
62     }
63 </script>
64 </html>

JQuery-zTree.js使用范例_子节点_02

如上,实现默认选中,在初始化树后立即调用处理默认选中的方法即可。我们模拟默认选中的数据中包含了id和name以及pid,这些都是比较基础的数据,ztree的selectNodecheckNode方法都是根据id实现选中的,
默认选中要提供的数据和渲染树用的格式是相同的。其中:

  1. getZTreeObj()将根据<div>中定义的id值来获取当前树的对象;
  2. selectNode实现选择节点,不会勾选节点,但是它能实现将被勾选的子节点所在的父节点展开;
  3. checkNode实现勾选节点,设置第三个参数是false,则表示选中父节点时不联动勾选其下的子节点,因为子节点未必都要默认选中。

如何获取选中的节点

获取选中的节点,只需要了解zTree.getCheckedNodes(),用来获取选中节点数据的JSON对象。其中获取到的选中节点数据包含一定顺序:选中父节点永远在选中子节点的最前面。

如果想要在提交表单的时候,将选中节点的值传给后台,就可以使用getCheckedNodes()方法获取到选中节点数据,然后遍历得到各个选中节点的数据。

如何实现单选

实现单选,只需要在settingcheck中配置chkStyle: "radio"即可实现单选,但是,此时实现的单选只在同级节点上才能实现单选,也就是说你在同级节点上只能单选,但是在你可以同时选中子节点和父节点。

那么,在你调用getCheckedNodes()方法获取选中节点数据时,其中也包含了选中的父节点,因为父节点可能只是个分组不一定要存入到数据库中;那么此时你就要判断下如果选中的节点的长度>0,那么就取索引位置的最后一个值;

后端如何封装Tree树结构

上面我们将的都是前端如何将JSON数据渲染成一棵Tree树,但是渲染用的数据应该是冲数据库中读取的。下面我们应该学习一下后端如何实现封装Tree树用的JSON数据。

  • SpringMVC

我们使用SpringMVC作为与后端交互的Web层框架,关于SpringMVC + Spring + Mybatis 框架的整合,大家可以参看我的这个项目 SSM框架整合

封装实体类

想必大家一定知道@ResponseBody这个注解,如果方法或类上添加了这个注解,那么@RequestMapping()映射return的东西将不再是InternalResourcecViewResolver视图解析器解析的视图地址,而是JSON格式的数据。
那么想要让SpringMVC相应一串[{"id": "xx", "name": "", "xx"},{"id": "xx", "name": "xx"}]这种格式的数据,我们就必须手动将数据封装成这种格式,如此SpringMVC才能将对象转换成JSON串。

我们会想到,我们可以将从数据库中读取的数据,依次存入到Map(或List)集合中,然后return map。当然,这是可行的,但是或许麻烦了些,因为整个项目中不止要构建一棵Tree树,每次都要new Map重用率就太低了。所以,一个简单的方式,就是手动创建一个实体类TreeEntity.java用以存放从数据库中读取到的数据,这样每次构建Tree树都能使用这个实体类对象。

TreeEntity.java属性如下:


 1 public class TreeEntity implements Serializable {
 2     private Long id;  //节点的id值
 3     private String name; //节点的名称
 4     private Boolean isParent; //是否是父节点
 5     private Long pid; //当前节点对应父节点的id值
 6 
 7     public TreeEntity(Long id, String name, Boolean isParent, Long pid){
 8       this.id = id;
 9       this.name = name;
10       this.isParent = isParent;
11       this.pid = pid;
12     }
13     
14     getter/setter ....
15 }

如上,你会发现,这是不是和我们前面说的前端构建Tree树的结构是一样的呢。没错,我们前端既然定义了这种格式,后端就必须要给它一个这样格式的数据。

Web层封装Tree数据结构

  • 注: 本例中涉及的表设计请参考我的这篇博文 Shiro实现权限管理之表结构设计 。

这里不再讲Dao层中如何查询的数据,我们仅以一个最简单的查询(findAll查询所有)来讲述Tree的数据结构封装。

先看代码:

 1 @ResponseBody
 2 @RequestMapping("/getZTreeForUserRoles")
 3 public List<TreeEntity> getTreeForUserRoles() {
 4     try {
 5         List<TreeEntity> treeList = new ArrayList<TreeEntity>();
 6         List<Role> roleList = roleService.findAll();
 7 
 8         for (Role role : roleList) {
 9             // 为tree树节点添加数据,节点pid为0的都是父节点,其他为子节点
10             if(role.getPid() != null){
11                 if (role.getPid() == 0) {
12                     treeList.add(new TreeEntity(role.getId(), role.getDescription(), true, (long) 0));
13                 } else {
14                     treeList.add(new TreeEntity(role.getId(), role.getDescription(), false, role.getPid()));
15                 }
16             }
17         }
18         return treeList;
19     } catch (Exception e) {
20         e.printStackTrace();
21         return null;
22     }
23 }

解释

  1. 首先要定义映射方法返回的数据类型是List<TreeEntity>;即返回的是一个List集合,但是其中存的是TreeEntity实体类的数据。
  2. 初始化一个空的List集合new ArrayList<TreeEntity>();并调用Service层的方法获取到sys_roles表中的所有数据,当然findAll()方法的返回值也是List集合。
  3. 遍历findAll()查询到的数据;这就体现了返回值是List并且泛型是实体类的优势了,这样我们可以直接通过实体类中定义的setter/getter来存取数据。
  4. 调用TreeEntity中定义的带参构造方法,将3中遍历得到的数据依次循环啊添加到List<TreeEntity>集合中。
  5. List<TreeEntity>集合返回。

我们来看一下这个请求映射返回的数据格式是如何的:

JQuery-zTree.js使用范例_父节点_03

如上,我们已经实现了目的。

拓展

上面的代码中还要说明的就是调用TreeEntity的带参构造函数传入的参数值。我们定义的带参构造函数如下:

1 public TreeEntity(Long id, String name, Boolean isParent, Long pid) {
2     this.id = id;
3     this.name = name;
4     this.isParent = isParent;
5     this.pid = pid;
6 }

在为List<TreeEntity>集合循环添加值时,要弄清楚:

  1. 节点id是什么?

节点的id是每个节点的唯一标识,就像数据库的主键值一样,所以我们通常将其设置为数据库中的主键值。并且以后也要获取这个主键值。

  1. 节点名称是什么?

节点的名称name是前端展示的各个节点的名称。而这些名称应该和数据库中的值是相同的,所以我们将其设置为数据库的description的值。

  1. 父节点是什么?

父节点,我们在数据库中已经定义了,即数据库中存在一个字段pid,这个字段表示的是上级节点的id值,即如果存在上级节点(或叫上级分组),那么就给此row的pid字段设置为上级row的主键id值。

  1. 如何定义父节点?

根据构造方法中的isParent字段,如果是父节点就直接手动设置为true,否则就设置为false。
如何判断是父节点?根据数据库(实体类)中已有的属性值pid判断,如果pid不为0就表示是子节点,如果pid是0就是父节点(因为主键值不可能为0)。