众所周知,layui.tree的树形控件,在渲染树节点时,需要后台准备好整个组织树的数据;如果组织树的数据量特别大,页面渲染就特别慢。最近在狐小E智慧办公平台中,展示企业部门树时,就遇到这种问题;当时产品要求,部门树渲染要特别快,同时某部门下新增/删除了一个子部门时,要动态刷新,实时呈现;然而layui.tree的原始机制是,必须重新加载整个树的数据再渲染,这样就会导致展开的节点全收缩回去,而不是刚好展开到该父部门位置;
这样就有2个问题亟需优化:
1)部门树数据量大时,渲染慢。
2)父部门如何动态刷新子部门,并且局部更新视图;
为了解决这2个问题,我们只好修改layui.tree源码,实现这种高大上的功能;废话不多说,直接上干货:
狐小E智慧办公后台使用的layui版本是v2.5.5,其他版本的layui源码可能有所不同,这点需注意。
解决方案
步骤1:源码修改
首先,在layui前端框架找到实现树组件的模块源码tree.js,如图所示:
1:打开源码,在树的主渲染方法 r.render前,添加上用于加载child节点的方法r.children,代码如下:
r.children = function (e, i, d) {
var a = l.that[e];
return a.children(i, d)
}
效果如下图:
2:找到b.prototype.tree方法,在此方法前,添加两个用于懒加载子节点的方法:b.prototype.children = function (n1, n2) {
var e = this;
e.setchildrendata(e.config.data, n1, n2);
},
b.prototype.setchildrendata = function (n0, n1, n2) {
var e = this;
var c = i('#' + e.config.id);
layui.each(n0, function (a, r) {
var b = c.find('div[data-id=' + r.id + ']').hasClass(C);
r.spread = b;
if (e.config.accordion === !0) { //手风琴模式
r.spread = !1;
var cs = c.find('div[data-id=' + n1 + ']').parents('.layui-tree-set');
cs.each(function () {
if (r.id === i(this).attr('data-id')) {
r.spread = !0;
}
});
}
if (r.id === n1) {
r.spread = !0;
if(!r.children){
r.children=[];
}
if (n2.length === 0) {
delete r.children;
}else{
//i.extend(!0, r.children, n2);
r.children = n2;
}
e.reload(e.config.id, e.config.data);
}
if (r.children) {
e.setchildrendata(r.children, n1, n2); //递归子节点
}
});
}
3:修改b.prototype.tree方法的代码,修改后代码如下【备注://TODO 部分标识的是layui原始代码】
b.prototype.tree = function (e, a) {
var n = this, t = n.config, r = a || t.data;
layui.each(r, function (a, r) {
//TODO 此处调整源码
//var l = r.children && r.children.length > 0,
var l = r.children,
o = i(''),
//TODO 源码调整
//h = i(['', '', '', function () {
h = i(['', '', '', function () {
//TODO 源码调整
//return t.showLine ? l ? '' + (r.title || r.label || t.text.defaultNodeName) + "" : '' + (r.title || r.label || t.text.defaultNodeName) + ""
return t.isJump && r.href ? '' + (r.title || r.label || t.text.defaultNodeName) + "" : '' + (r.title || r.label || t.text.defaultNodeName) + ""
}(), "", function () {
if (!t.edit) return "";
var e = {
add: '',
update: '',
del: ''
}, i = [' ' : ' ' : ' '
return t.showLine ? l ? ' ' : ' ' : ' '
}(), function () {
return t.showCheckbox ? '' : ""
}(), function () {
//TODO 修改源码显示title
//return t.isJump && r.href ? ''];
return t.edit === !0 && (t.edit = ["update", "del"]), "object" == typeof t.edit ? (layui.each(t.edit, function (a, n) {
i.push(e[n] || "")
}), i.join("") + "") : void 0
}(), ""].join(""));
l && (h.append(o), n.tree(o, r.children)), e.append(h), h.prev("." + s)[0] && h.prev().children(".layui-tree-pack").addClass("layui-tree-showLine"), l || h.parent(".layui-tree-pack").addClass("layui-tree-lineExtend"), n.spread(h, r), t.showCheckbox && (r.checked && n.checkids.push(r.id), n.checkClick(h, r)), t.edit && n.operate(h, r)
})
}
4:修改节点展开方法b.prototype.spread,修改后代码如下【备注://TODO 部分标识的是layui原始代码】
b.prototype.spread = function (e, a) {
var n = this, t = n.config, r = e.children("." + p), l = r.children("." + f), c = r.find("." + o),
k = r.find("." + y), m = t.onlyIconControl ? c : l, x = "";
m.on("click", function (i) {
var ax = e.children("." + v),//TODO a冲突改成ax
//TODO 修改源码
//n = m.children(".layui-icon")[0] ? m.children(".layui-icon") : m.find(".layui-tree-icon").children(".layui-icon");
n = m.children(".layui-icon")[0] ? m.find(".layui-tree-iconClick").children(".layui-icon") : m.find(".layui-tree-icon").children(".layui-icon");
if (ax[0]) { //TODO a改为ax
//TODO 展开节点
if (!e.hasClass(c) && !e.hasClass(C)) {
t.spread && t.spread({
elem: e,
state: a.children.length > 0,
data: a
})
}
//TODO 注释掉源码
/*if (e.hasClass(C)) e.removeClass(C), a.slideUp(200), n.removeClass(u).addClass(h); else if (e.addClass(C), a.slideDown(200), n.addClass(u).removeClass(h), t.accordion) {
var r = e.siblings("." + s);
r.removeClass(C), r.children("." + v).slideUp(200), r.find(".layui-tree-icon").children(".layui-icon").removeClass(u).addClass(h)
}*/
//TODO 节点展开效果调整添加Begin
if (e.hasClass(C) && !$(i.target).hasClass("layui-tree-txt")) {
e.removeClass(C), ax.slideUp(200)
//修改源码,调整无实线状态时三角图标有动画
if (!t.showLine) {
m.find('span .layui-icon').removeClass(aa).addClass(bb)
} else {
n.removeClass(u).addClass(h)
}
} else if (e.addClass(C), ax.slideDown(200), n.addClass(u).removeClass(h), t.accordion) {
var r = e.siblings("." + s);
r.removeClass(C), r.children("." + v).slideUp(200), r.find(".layui-tree-icon").children(".layui-icon").removeClass(u).addClass(h)
} else if (!t.showLine) {
m.find('span .layui-icon').removeClass(bb).addClass(aa)
} //TODO 节点展开效果调整添加End
} else x = "normal"
})
5:b.prototype.spread 方法修改中,有两处样式添加 aa 、bb:如下所示
需要在tree.js中顶部样式变量处添加。
步骤2:方案落地
解决问题1:异步加载子节点
下面是狐小E智慧办公 (https://www.hixiaoe.com )后台通讯录模块中,部门树渲染加载的业务逻辑,其核心逻辑就是通过父节点ID查询子节点数据,绑定tree组件的spread函数,捕捉节点的展开事件,用户点击树节点,根据当前节点ID查询下一级节点,如果children节点有数据,则调用tree的children函数动态地渲染子节点。
//获取部门树
function LoadDeptTree() {
$.ajax({
url: "${ctx}/dept/tree/one-level?parentId=0",
dataType: "json",
async: true,
type: "GET",
success: function (resp) {
if (resp && resp.msgcode == 0) {
//无连接线风格
deptTree =tree.render({
elem: '#leftTreeArea'
,id:'leftTreeArea'
, data: resp.data
, showLine: false //是否开启连接线
, click: editDept
, spread: function (obj) {
if (!obj.state) {
// 懒加载子节点,异步获取data数据这里根据obj.data.id向后台请求当前节点数据
$.ajax({
url: "${ctx}/dept/tree/one-level?parentId="+obj.data.id,
dataType: "json",
async: true,
type: "GET",
success: function (resp) {
//当前节点展开,如果下一层有children,则调用树的children方法,动态渲染子节点
if (resp && resp.msgcode == 0) {
//第一个参数是树绑定的页面元素ID
//第二个参数是当前展开节点的ID
//第三个参数是当前节点子节点的数据(数据格式参照layui的tree组件数据格式)
tree.children(deptTree.config.id, obj.data.id, resp.data);
}
}
});
}
}
});
} else {
alert("加载部门树失败")
}
},
error:function(XMLHttpRequest, textStatus, error){
if (error.code == 19) {
window.location.reload();
}else{
alert("加载部门树失败");
}
}
});
}
解决问题2:父部门添加子部门,局部动态更新视图
选中父部门,动态地为父部门添加一个子部门,添加成功将新增的子部门局部渲染到整体部门树中。下面是添加子部门后,js调用一下我自己的refreshTreeNode函数,函数里有操作layui.tree的局部更新视图的代码,如下:
如果想下载我们修改后的代码,请点击:
https://oa.hixiaoe.com/static/layui-v2.5.x/src/lay/modules/tree.js
作者介绍:小文文,狐小E智慧办公 (https://www.hixiaoe.com )开发工程师,专注移动办公软件的SaaS平台建设以及轻应用开发