为Layui的tree组件添加懒加载生成节点功能(支持其tree组件绑定的默认方法)
需求
- 工作中使用了Layui搭建页面,其中有一个功能是下拉框支持树形结构得到数据,且支持多选;
- 但是树形结构的数据不是一次性返回,而是每次掉接口返回一个节点数据(后端接口接收一个参数: 父节点的ID, 返回该父节点下直系的子节点数据),不仅需要递归多次,且一次性掉了很多次的接口,性能不好,而且这个接口每分钟还存在调用次数上限;
- 所以需要动态加载节点(或者说懒加载吧,我也不知道咋说🤣),反正就是数据中会添加一个字段:hasChildren,如果有这个字段的话文字左边就会可点击的 ? 图标,点击之后调接口根据返回的数据渲染节点
- 支持复选框多选
实现1(粗暴的直接操作DOM,模拟layui的css):
根据返回的数据动态生成DOM添加至节点中,其弊端就是没有通过Layui tree组件的渲染,所以不支持tree组件自带的回调来获取参数,需要自己给DOM添加绑定事件,也不支持上面的需求4(或者说实现极其复杂,还要动态生成复选框),这是一个试错的过程(我是先实现1,因为太复杂了才想到了实现2),有兴趣的可以看看,没时间的可以跳过看实现2😂
先实例化tree组件,获取初始数据
初始代码如下所示: 
页面显示如下:  可以看到因为初始数据 treeData 中没有children字段,其节点不可展开(连展开按钮都没有),这时候我想的是根据hasChildren 这个字段模拟一个假的children数据chilren: [{title: '',id: ''}] 来让其可以展开来;这里我模拟一下调接口:  这时候我们再看页面就变成了:  初始效果有了,接下来要实现的就是点击展开图标,调接口获取真的children数据,将里面的假children数据替换掉,为什么上面的截图我将浏览器开发者工具打开且将Elements里面的节点全部展开呢?其实就是为了接下来的操作,当时我想的就是模拟它的dom结构和CSS类名来实现效果,后来发现还要重新绑定事件实现交互功能,****😭(原理就是照葫芦画瓢 生成DOM 绑定事件)
document.getElementById('tree_dep').addEventListener('click', function(e){
if (e.target.classList.contains('layui-icon-addition')) {
if (e.target.classList.contains('custom-addition')) {
addition(e)
} else {
subtraction(e)
}
}
if (e.target.classList.contains('layui-icon-subtraction')) {
if (e.target.classList.contains('custom-addition')) {
subtraction(e)
} else {
addition(e)
}
}
}, false)
function addition(e) {
let p_dom = e.target.parentNode.parentNode.parentNode.nextSibling
let b_dom = e.target.parentNode.parentNode.parentNode.parentNode
let txt = e.target.parentNode.nextSibling.innerText
let data_id = b_dom.getAttribute('data-id')
if (tree_open[data_id]) {
p_dom.style.display = 'none'
b_dom.classList.remove('layui-tree-spread')
e.target.classList.remove('layui-icon-subtraction')
e.target.classList.add('layui-icon-addition')
tree_open[data_id] = false
return
}
tree_open[data_id] = true
if (!lazy_loading[data_id]) {
do_async(data_id).then(res => {
b_dom.innerHTML = `<div class="layui-tree-entry">
<div class="layui-tree-main">
${res.length? '<span class="layui-tree-iconClick layui-tree-icon"><i class="layui-icon layui-icon-subtraction"></i></span>' : '<span class="layui-tree-iconClick"><i class="layui-icon layui-icon-file"></i></span>'}<span class="layui-tree-txt">${txt}</span>
</div>
</div>`
recur_obj(treeData, data_id, res)
if (res.length !== 0) {
let pack_dom = document.createElement('div')
pack_dom.classList.add('layui-tree-pack', 'layui-tree-lineExtend', 'layui-tree-showLine')
pack_dom.style.display = 'block'
for (let i = 0;i < res.length;i++) {
let item = res[i]
let div_dom = document.createElement('div')
div_dom.setAttribute('data-id', item.id)
div_dom.classList.add('layui-tree-set')
div_dom.innerHTML = `<div class="layui-tree-entry">
<div class="layui-tree-main">
${item.hasChildren ? '<span class="layui-tree-iconClick layui-tree-icon"><i class="layui-icon layui-icon-addition custom-addition"></i></span>' : '<span class="layui-tree-iconClick"><i class="layui-icon layui-icon-file"></i></span>'}<span class="layui-tree-txt">${item.title}</span>
</div>
</div>`
if (item.hasChildren) {
lazy_loading[item.id] = false
}
pack_dom.appendChild(div_dom)
}
b_dom.appendChild(pack_dom)
lazy_loading[data_id] = true
}
})
}
}
function subtraction(e) {
let p_dom = e.target.parentNode.parentNode.parentNode.nextSibling
let b_dom = e.target.parentNode.parentNode.parentNode.parentNode
let txt = e.target.parentNode.nextSibling.innerText
let data_id = b_dom.getAttribute('data-id')
if (!tree_open[data_id]) {
p_dom.style.display = 'block'
b_dom.classList.add('layui-tree-spread')
e.target.classList.remove('layui-icon-addition')
e.target.classList.add('layui-icon-subtraction')
return
}
tree_open[data_id] = false
}
function recur_obj(data, id, arr) {
for (let i = 0;i < data.length;i++) {
if (data[i].id == id) {
data[i].children = arr
return
}
if (data[i].children && data[i].children.length) {
recur_obj(data[i].children, id, arr)
}
}
}
实现的效果:  按照这个思维,接下来我们就是需要重新给DOM绑定事件,并且根据需求还要生成复选框元素,反正就是很复杂,头都秃了,Layui的文档里面也没有懒加载动态生成节点,最多只有操作节点。。。。  等等,操作节点中有增加节点的功能,那岂不是源码中有新增节点的功能代码,因此就有了 实现2
实现2(在源码中添加懒加载功能代码)
首先我使用的是从Layui官网下载的自动化构建后的代码,没办法看更别说改了,所以第一步我们从git仓库下载完整开发包 Layui官网链接  GitHub里面我们下载zip,下载之后解压,解压之后是这样的目录结构:  用vscode打开,进入src里面看了一下layui.js,这应该是入口文件吧,然后找到modules文件夹里面的tree.js 文件,我们就在这里面给我们的tree 组件添加新功能;首先我们要找到自带的新增节点的功能代码: 如上图所示,我们找到了这块代码,看一下点击新增节点图标tree.js到底做了那些; 然后定义一个我们期望传入的数据结构,以及如何在tree.render()里面增加懒加载的基础参数:
let data = [
{
title: '****',
id: '****',
hasChildren: true,
children: [],
}
]
layui.use('tree', function(){
var tree = layui.tree;
tree.render({
lazyLoad: function(){},
})
})
然后我们开始在tree.js里面动工,首先如果数据源里面没有children字段但是hasChildren为true时,我们希望有可展开的图标  有可展开图标了,但是没有数据(即hasChildren为true,children没有或者为空数组)我们就判定这是个懒加载节点,这时候我们给 +图标 加个事件,在如下代码里面加  圈的是我们自定义的懒加载事件,源代码中没有这个if判断,我们加一个提前判断,接收我们传入的lazyLoad异步函数,如果没有传就会报错
if (item.hasChildren && !item.children) {
var async_tree = options.lazyLoad || function () {throw new Error('The ASYNC_TREE option is nota found in the ' + MOD_NAME + ' instance')};
async_tree(item.id).then(function(res){
elem.append('<div class="layui-tree-pack"></div>');
if(options.showLine){
if(!packCont[0]){
var siblings = elem.siblings('.'+ELEM_SET), num = 1
,parentPack = elem.parent('.'+ELEM_PACK);
layui.each(siblings, function(index, i){
if(!$(i).children('.'+ELEM_PACK)[0]){
num = 0;
};
});
if(num == 1){
siblings.children('.'+ELEM_PACK).addClass(ELEM_SHOW);
siblings.children('.'+ELEM_PACK).children('.'+ELEM_SET).removeClass(ELEM_LINE_SHORT);
elem.children('.'+ELEM_PACK).addClass(ELEM_SHOW);
parentPack.removeClass(ELEM_EXTEND);
parentPack.children('.'+ELEM_SET).last().children('.'+ELEM_PACK).children('.'+ELEM_SET).last().addClass(ELEM_LINE_SHORT);
}else{
elem.children('.'+ELEM_PACK).children('.'+ELEM_SET).addClass(ELEM_LINE_SHORT);
};
}
elemMain.find('.'+ICON_CLICK).addClass('layui-tree-icon');
elemMain.find('.'+ICON_CLICK).children('.layui-icon').addClass(ICON_ADD).removeClass('layui-icon-file');
}else{
elemMain.find('.layui-tree-iconArrow').removeClass(HIDE);
};
item.children = res
that.tree(elem.children('.'+ELEM_PACK), res);
if(options.showCheckbox) {
if(elemMain.find('input[same="layuiTreeCheck"]')[0].checked){
var packLast = elem.children('.'+ELEM_PACK).children('.'+ELEM_SET).last();
packLast.find('input[same="layuiTreeCheck"]')[0].checked = true;
};
that.renderForm('checkbox');
};
elem.addClass(ELEM_SPREAD);
elem.children('.'+ELEM_PACK).slideDown(200);
var iconClick = touchOpen.children('.layui-icon')[0] ? touchOpen.children('.layui-icon') : touchOpen.find('.layui-tree-icon').children('.layui-icon');
iconClick.addClass(ICON_SUB).removeClass(ICON_ADD);
}).catch(function(rej){
console.log(rej)
})
}
最终我们定义数据源和基础参数如下所示
let treeData = null,
lazy_loading = {},
tree_open = {};
treeData = [{
description: "集团总部",
hasChildren: true,
id: "000001",
title: "集团总部",
}]
let asyncData = {
"000001": [
{description: "北京分部", hasChildren: true, id: "000002", title: "北京分部"},
{description: "南京分部", hasChildren: true, id: "000003", title: "南京分部"},
{description: "济南分部", hasChildren: true, id: "000004", title: "济南分部"},
{description: "广州分部", hasChildren: true, id: "000005", title: "广州分部"},
{description: "天津分部", hasChildren: true, id: "000006", title: "天津分部"},
{description: "合肥分部", hasChildren: true, id: "000007", title: "合肥分部"},
{description: "深圳分部", hasChildren: true, id: "000008", title: "深圳分部"},
{description: "武汉分部", hasChildren: true, id: "000009", title: "武汉分部"},
{description: "上海分部", hasChildren: true, id: "000010", title: "上海分部"},
{description: "杭州分部", hasChildren: true, id: "000011", title: "杭州分部"},
{description: "西安分部", hasChildren: true, id: "000012", title: "西安分部"}
],
"000002": [
{"description":"北京分部-职能部门", "hasChildren":false, "id":"000002-001", "title":"职能团队"},
{"description":"北京分部-项目团队", "hasChildren":false, "id":"000002-002", "title":"项目团队"},
{"description":"北京分部-管理部门", "hasChildren":false, "id":"000002-003", "title":"管理部门"},
{"description":"北京分部-后勤部门", "hasChildren":true, "id":"000002-004", "title":"后勤部门"},
{"description":"北京分部-销售团队", "hasChildren":false, "id":"000002-005", "title":"销售团队" },
{"description":"北京分部-财政部门", "hasChildren":false, "id":"000002-006", "title":"财政部门"},
{"description":"北京分部-渠道团队", "hasChildren":false, "id":"000002-007", "title":"渠道团队"},
{"description":"北京分部-福利员工部", "hasChildren":false, "id":"000002-008", "title":"福利员工部"}
]
}
layui.use('tree', function(){
tree = layui.tree;
tree.render({
elem: '#tree_dep',
id: 'tree_dep',
data: treeData,
accordion: true,
onlyIconControl: true,
showCheckbox: true,
lazyLoad: function (id) {
return new Promise(function(res, rej){
if (!asyncData[id]) {
alert('获取失败')
rej('errorMsg')
} else {
setTimeout(function () {
res(asyncData[id])
}, 20)
}
})
},
click: function(obj){
console.log(obj.data)
}
})
})
实际效果:  这样就改好了,其他配置按照Layui官网里面的配置就行,事件的话也是完美兼容的(起码我测试是这样的😉,如果有测试出来问题的话,评论区可以讨论交流🧐);
代码的话我也贴一下(就和官网开始使用的方法一是一样的目录结构,向它介绍的那样引用就行了):代码压缩包 (备注:没有积分的小伙伴想要的话给我发消息就好了😗),大家想要实现的话也可以按照这个思路动手试一试
说到这里就要讲讲我用它默认的gulp自动化构建的时候出现的两个问题:
- 首先是node版本过高,报错:primordials is not defined,后来我用nvm将node 版本降为v8.7.0就不报错了,但是出现了问题2
- 原因竟然是因为箭头函数写顺手了,在tree.js里面写了两个箭头函数,它这个源码里用的都是es5的语法,我用了es6的箭头函数导致它编译出错,后来引用了gulp-babel等插件还是解决不了,我就干脆把箭头函数改为function(){}了,实在是找不出来解决办法😭

|