ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

jEasyUI TreeGrid实战:树形网格配置、数据格式与懒加载优化

jEasyUI TreeGrid实战:树形网格配置、数据格式与懒加载优化 最近在做一个后台管理系统需要把组织架构和权限树放在同一个列表里展示还要支持逐级展开、直接在某一行上做操作。这种需求最合适的方案就是用树形网格TreeGrid。我选的是 jEasyUI 的 treegrid 组件整体做下来体验不错但也有不少坑。这篇文章我把自己实际使用的过程、配置参数、数据格式和排查经验都整理出来给准备上手 jEasyUI 树形网格的同学做个参考。1. 树形网格是什么跟普通表格有什么区别先说说树形网格到底是个什么东西。普通表格datagrid是扁平结构一行一条数据数据之间没有父子关系。而树形网格treegrid在表格的行上增加了树形层级每一行既可以像普通表格一样展示多列数据又可以通过展开、折叠查看子级数据。最常见的场景就是组织架构管理、商品分类管理、地区级联选择、权限菜单配置。我自己在项目里最直观的感受是如果用一个普通表格展示组织架构只能把所有部门列出来上下级关系靠“上级部门”这个字段去脑补如果用树形组件则只能展示节点名称无法展示每个部门的编制人数、负责人、创建时间等详细信息。treegrid 把两边结合了既能看到层级又能像操作表格一样对每一行的多列数据进行查看和编辑这就是它在后台管理系统里非常“能打”的原因。1.1 jEasyUI 里 treegrid 的核心价值jEasyUI 是一套基于 jQuery 的 UI 组件库treegrid 是其中功能非常完整的一个组件。它的核心价值可以归纳为三点数据驱动树形层级完全由数据的 parentId 或 children 字段决定前端不拼接、不递归生成 HTML组件内部自动处理展开、折叠、缩进对齐。功能复用继承了 datagrid 的列定义、格式化、行选中、分页虽然树形分页不是必备、工具栏等能力学习成本低。懒加载支持子节点可以按需向后端请求不需要一次性把整棵树的数据量压给浏览器数据量大时性能表现稳定。我实际用的项目数据量大概在 2000 个节点左右一次性全量加载时渲染速度依然可以接受但如果你的数据过万建议采用懒加载后面我会详细讲。1.2 适合用 treegrid 的典型场景从业务角度说我建议在下面这些场景优先考虑 treegrid而不是自己用 datagrid 加缩进拼父子关系需要同时展示层级和多列数据。比如一个商品分类树每一行要显示分类编码、分类名称、排序值、上架状态、操作按钮。需要逐级操作子节点。比如部门管理里先在父级部门那一行点击展开再对某一个子部门单独做编辑或停用。需要在层级列表中直接编辑单元格。treegrid 支持在列上配置 editor点一下单元格就能进入编辑状态。如果你的需求只是展示一棵简单的树没有多列数据那么用 jEasyUI 的 tree 组件就足够了不需要上 treegrid。2. 创建一个最基础的树形网格这一部分我从零开始搭建一个可运行的基础 demo。为了让例子更完整我直接使用静态 JSON 数据模拟后端返回你在实际项目中把这段数据换成接口请求结果即可。2.1 页面结构准备jEasyUI 依赖 jQuery所以页面里需要引入三个东西jQuery 核心库、jEasyUI 的核心 JS、jEasyUI 的主题 CSS。!DOCTYPE html html head meta charsetutf-8 titlejEasyUI 基础树形网格/title link relstylesheet typetext/css hrefthemes/default/easyui.css link relstylesheet typetext/css hrefthemes/icon.css script typetext/javascript srcjquery.min.js/script script typetext/javascript srcjquery.easyui.min.js/script /head body table idtreeTable/table /body /html需要注意jEasyUI 对 jQuery 的版本有一定要求我用的组合是 jQuery 1.11.3 搭配 jEasyUI 1.9.x稳定性很好。建议别直接上 jQuery 3.x 搭配老版本 easyui容易出现兼容性报错。2.2 表格容器与初始化初始化 treegrid 有两种常见方式。第一种是直接在 table 标签上写>table idtreeTable classeasyui-treegrid >$(#treeTable).treegrid({ url: getData.json, method: get, animate: true, idField: id, treeField: name, columns: [[ { field: name, title: 名称, width: 220 }, { field: type, title: 类型, width: 100 }, { field: status, title: 状态, width: 80 } ]] });这里有两个配置项是 treegrid 的灵魂idField 和 treeField。idField当前行的唯一标识字段组件依赖它来识别每一行节点。如果数据中有 id 字段就填 id。treeField哪一个列作为树节点列展开箭头和缩进都渲染在这一列上。它必须是你 columns 里定义过的 field。2.3 基础数据格式jEasyUI treegrid 接受的静态数据有两种常见格式嵌套 children 格式和扁平 parentId 格式。直接看代码更直观。嵌套 children 格式[ { id: 1, name: 总裁办, type: 部门, status: 启用, children: [ { id: 2, name: 行政秘书组, type: 小组, status: 启用 }, { id: 3, name: 法务组, type: 小组, status: 停用 } ] }, { id: 4, name: 技术中心, type: 部门, status: 启用, children: [ { id: 5, name: 后端组, type: 小组, status: 启用 } ] } ]扁平 parentId 格式[ { id: 1, name: 总裁办, type: 部门, parentId: 0 }, { id: 2, name: 行政秘书组, type: 小组, parentId: 1 }, { id: 3, name: 法务组, type: 小组, parentId: 1 } ]两种格式组件都支持但要注意使用扁平 parentId 格式时需要额外对数据进行一次转换或者让后端直接返回带 children 的嵌套结构。我自己习惯让后端返回嵌套结构减少前端处理成本。3. 核心配置项与交互细节基础 demo 跑起来之后你会开始关注一些交互细节比如加载提示、默认展开层级、行点击事件、懒加载等。这些配置直接影响用户体验和代码健壮性。3.1 关键配置项逐项解析我把实际项目中经常用到的配置项整理成了一个速查表方便你对照使用。配置项类型默认值说明urlstringnull远程数据加载地址methodstringpost请求方式建议用 get 或 post 均可queryParamsobject空每次加载时额外携带的参数idFieldstringnull行 ID 字段名必填treeFieldstringnull树节点字段名必填animatebooleanfalse展开折叠时是否有动画rownumbersbooleanfalse是否显示行号fitColumnsbooleanfalse是否自适应列宽填满容器paginationbooleanfalse是否显示分页树形网格可配合使用loadMsgstring加载中加载过程的提示文字loadFilterfunctionnull自定义返回数据的过滤函数这里我要提醒一句fitColumns 在 treegrid 上要谨慎使用。因为树节点列通常需要一定宽度来展示缩进和箭头如果 fitColumns 为 true且列很多树节点列可能被压缩到很难看。我通常的做法是给树节点列设置较大的固定宽度其他列设置较小宽度或者干脆不启用 fitColumns。3.2 默认展开层级与全部折叠项目里有一种常见需求页面加载后树默认只展示顶级节点或者默认展开第一层。treegrid 的 loadFilter 可以在这里派上用场。在数据过滤函数里只给需要展开的节点设置 state组件会自动读取这个字段。jEasyUI 约定节点对象中设置 state: open 表示默认展开设置为 closed 表示默认折叠。$(#treeTable).treegrid({ url: getData.json, idField: id, treeField: name, loadFilter: function(data) { // 只展开第一级节点的子节点 if (data) { for (var i 0; i data.length; i) { if (data[i].children data[i].children.length 0) { data[i].state open; } } } return data; } });这里我试过一个小技巧如果想让整棵树默认全部展开只需要递归遍历所有带 children 的节点并设置 state 为 open 即可。但如果节点数量非常大全部展开会导致页面渲染压力大动画也会一卡一卡的。所以默认展开层级最好控制在两层以内。3.3 行点击、节点展开等事件处理事件绑定方面treegrid 提供了 onSelect 行选中、onExpand 节点展开、onCollapse 节点折叠、onBeforeExpand 节点展开前等事件。实际开发中我用得最多的是 onExpand 和 onSelect。比如在权限分配页面用户点击展开某个部门节点时需要把该部门下的全部子部门在指定区域列出来这时可以监听 onExpand 事件。$(#treeTable).treegrid({ onExpand: function(row) { // row 是当前展开的节点 console.log(展开节点, row.name); loadSubDepartments(row.id); } });再比如用户点击某一行时需要联动右侧表单可以监听 onSelect$(#treeTable).treegrid({ onSelect: function(row) { $(#detailForm).form(load, buildDetailData(row)); } });需要特别注意的是onSelect 在展开箭头点击时不会触发它只响应整行的选中。这一点和普通 datagrid 的行选中逻辑是一致的。3.4 懒加载按需请求子节点当数据量达到一定程度一次返回整棵树不仅网络开销大前端渲染也会卡顿。treegrid 可以通过 onBeforeExpand 配合 loadFilter在展开节点时才请求子级数据。做法是初始化时不加载 children后端只返回顶级节点每次展开节点时前端向接口发送当前节点 id后端返回该节点的直接子级。$(#treeTable).treegrid({ url: getTopLevelData.json, idField: id, treeField: name, onBeforeExpand: function(row) { // 如果当前节点是父级节点且尚未加载子节点就加载 var children $(#treeTable).treegrid(getChildren, row.id); if (children.length 0) { $.ajax({ url: getChildrenData.json, method: post, data: { parentId: row.id }, dataType: json, success: function(data) { // 把请求到的子节点追加到该节点下 $(#treeTable).treegrid(append, { parent: row.id, data: data }); } }); return false; // 返回 false 阻止默认的展开行为 } } });这里的关键是 return false。如果不 return false组件会走默认的 url 加载逻辑导致重复请求。如果你完全通过手动 ajax 加载子节点建议把 onBeforeExpand 作为唯一的数据加载入口。实际项目中我还会加一个 loaded 标记避免重复展开时二次请求。3.5 自定义操作按钮列后台管理系统的表格基本都少不了“操作”列treegrid 也不例外。操作列的渲染方式有两种选择formatter 函数或者直接在列里面拼按钮。我推荐用 formatter这样按钮的状态可以根据当前行数据动态控制。比如某些节点不允许删除就隐藏删除按钮$(#treeTable).treegrid({ idField: id, treeField: name, columns: [[ { field: name, title: 名称, width: 260 }, { field: type, title: 类型, width: 100 }, { field: status, title: 状态, width: 80 }, { field: operate, title: 操作, width: 160, formatter: function(value, row, index) { var editBtn a href# onclickeditNode(\ row.id \)编辑/a; var delBtn ; if (row.status ! 停用) { delBtn a href# onclickdeleteNode(\ row.id \)删除/a; } return editBtn delBtn; }} ]] });在事件处理函数里注意使用事件对象的 stopPropagation防止点击按钮时触发整行的选中事件。我习惯把行 id 传到函数里再通过 treegrid 的 getData 方法拿到整行数据。4. 完整案例部门组织架构树形网格理论说太多不如直接做一个完整案例。这一节我把一个部门组织架构管理页面的核心代码完整展示出来包含需求分析、前后端数据交互和前端实现。4.1 需求梳理与数据设计假设我们要做一个简单的部门管理页面包含如下需求展示部门层级关系每一行显示部门名称、部门编码、负责人、人数、状态。顶级部门默认展开一层。点击操作列中的“查看成员”按钮弹出窗口展示该部门下的人员列表。支持折叠和展开动画。后端接口约定如下GET /api/dept/tree返回整棵部门树。GET /api/dept/members?deptId1返回指定部门下的人员列表。返回的部门树数据格式[ { id: 1, deptName: 总裁办, deptCode: D001, manager: 张总, memberCount: 12, status: 启用, children: [ { id: 2, deptName: 行政部, deptCode: D00101, manager: 李主任, memberCount: 8, status: 启用, children: [] } ] } ]4.2 前端页面完整代码页面结构分为两部分左侧一个树形网格右侧一个用于展示成员信息的面板。这样页面信息密度高又不会互相干扰。div stylepadding: 10px; table iddeptTree/table /div div idmemberPanel styledisplay: none; table idmemberGrid/table /div$(function() { $(#deptTree).treegrid({ url: /api/dept/tree, method: get, idField: id, treeField: deptName, animate: true, rownumbers: true, loadMsg: 部门数据加载中..., loadFilter: function(data) { // 只展开第一层 for (var i 0; i data.length; i) { if (data[i].children data[i].children.length 0) { data[i].state open; } } return data; }, columns: [[ { field: deptName, title: 部门名称, width: 220 }, { field: deptCode, title: 部门编码, width: 120 }, { field: manager, title: 负责人, width: 100 }, { field: memberCount, title: 人数, width: 80, align: center }, { field: status, title: 状态, width: 80, align: center }, { field: operate, title: 操作, width: 120, align: center, formatter: function(value, row) { return a href# classviewMemberBtn>function buildTree(flatData, parentId) { var tree []; for (var i 0; i flatData.length; i) { if (flatData[i].parentId parentId) { var node flatData[i]; var children buildTree(flatData, node.id); if (children.length 0) { node.children children; } tree.push(node); } } return tree; }然后在 ajax 请求拿到数据之后执行一次转换再交给 treegrid$.get(/api/dept/all, function(flat) { var tree buildTree(flat, 0); $(#deptTree).treegrid(loadData, tree); });这个 buildTree 是递归实现数据层级很深最多也就十几层不用担心爆栈问题。5. 常见问题与排查技巧实录这部分是我实际使用 jEasyUI treegrid 过程中踩过的坑汇总很多问题在官方文档里并不会直接写清楚但一旦遇到就会让页面显示异常我把排查思路也一并写出来。5.1 树形层级不显示所有数据平铺这个问题我遇到得最多。通常是 idField 和 treeField 配置错误导致的。idField 填了不存在的字段组件无法识别父子关系就会把所有数据当成平级渲染。排查时先在浏览器控制台打印接口返回的原始数据确认 id 字段名到底是什么。有的后端返回的是 ID 大写或者 data 里包含 data 数据导致字段匹配不上。// 错误示例后端的 id 字段名是 ID不是 id $(#treeTable).treegrid({ idField: ID, // 没有实际匹配 treeField: name });另外如果数据里存在重复 id也会造成层级错乱。treegrid 要求每个节点的 id 在全树中唯一这一点要提前建立约束。5.2 数据加载失败但页面不报错treegrid 在加载失败时通常只是不显示数据控制台不一定有红色错误。排查步骤我总结为四步打开浏览器的 Network确认请求是否发出。查看响应状态码是否为 200。查看响应内容是否为合法 JSON。检查 loadFilter 是否过滤掉了全部数据比如对 data 的层级结构判断不完整。有一次我在 loadFilter 里写了个逻辑返回 data.rows但接口返回的是 data.list结果一直没有数据卡了半天才排查出来。所以 loadFilter 里的字段名要和后端严格对齐。5.3 展开节点后子节点不显示如果使用的是懒加载展开节点后子节点不显示优先检查两处onBeforeExpand 里手动 ajax 的返回值有没有正确拼接为 { parent: row.id, data: [...] } 的结构。手动 ajax 之后有没有 return false如果漏了组件会再次请求默认的 url造成两次请求且第二次的返回数据可能不是当前节点的子集。onBeforeExpand: function(row) { if (isLoaded(row.id)) { return; // 已经加载过直接放行 } $.get(/api/dept/children, { parentId: row.id }, function(resp) { if (resp.code 0) { $(#deptTree).treegrid(append, { parent: row.id, data: resp.data }); markLoaded(row.id); } }, json); return false; }我在这里加了一个 isLoaded 和 markLoaded 用来做已加载标记防止用户反复展开折叠节点时产生重复请求。实际开发中定义一个对象变量来装已加载的 id 集合即可。5.4 行选中与按钮点击事件冲突在操作列中放按钮时经常会遇到点击按钮却触发了整行选中的问题。解决办法有两个层面方法一是在按钮绑定的 click 事件里调用 stopPropagation$(document).on(click, .someBtn, function(e) { e.stopPropagation(); // 业务逻辑 });方法二是在 treegrid 的 onClickRow 事件中判断当前点击区域是不是操作列是的话直接 return。不过这种方式要判断列字段写起来比较繁琐我推荐第一种。5.5 加载后行号不连续如果同时启用了 rownumbers 和懒加载展开子节点后行号显示会有跳跃感因为子节点插入到了父节点下面。这是 treegrid 的正常行为并不会影响数据操作。如果对行号要求严格建议关闭 rownumbers改用前端自定义序号列。5.6 页面宽度自适应不佳treegrid 没有 datagrid 的 fit 属性那么方便时我一般通过给容器设置固定高度或者监听窗口 resize 事件手动调整$(window).resize(function() { $(#deptTree).treegrid(resize, { height: $(document).height() - topHeight }); });实测下来这个方式在旧项目中比较实用。5.7 实测性能优化心得如果节点总数超过 5000我强烈建议采用懒加载并且不要在树节点列使用过于复杂的 formatter。展开动画建议关闭因为动画在节点较多时会造成明显卡顿。此外用事件委托来绑定操作列按钮而不是每个按钮都直接绑定事件能显著减少初始化时间。给树节点列设置一个最小宽度同时开启自动截断也很关键。长文字会撑开列宽严重破坏整体布局。如果你用 easyui 的列配置可以在 formatter 里做字符串截断处理或者借助 CSS 的 overflow。6. 我从项目里总结的实践经验前面写了很多细节我最后再从全局角度分享几点做 treegrid 项目时的实践经验。第一数据格式的约定一定要前置。后端定义返回结构时最好直接约定为嵌套 children 结构避免前端再写转换函数。前端如果频繁转换数据既容易出 bug也会增加维护成本。第二treegrid 的配置最好统一封装。如果项目中有多个页面需要树形网格建议封装一个公共初始化函数把常见的 idField、treeField、animate、loadFilter 等配置统一处理每个页面只需要传 url 和 columns。这样可以避免每个页面都踩一遍同样的问题。第三关于编辑功能。如果你需要直接在 treegrid 单元格内编辑可以在列上配置 editor 属性结合 beginEdit 方法使用。但要特别小心树形结构在编辑状态下如果被编辑行带有子节点保存时一定要连带处理子节点的树形关系。我在一个分类管理页面里做过一次行内编辑保存时只改了当前行结果子节点的 parentId 全乱了后来通过整体重新加载数据才解决。第四注意组件版本。网上很多老教程都是基于 easyui 1.3、1.4 的写法和 1.9 以上版本的 API 存在差异。比如 loadFilter 的事件触发时机、append 的 parent 参数格式不同版本可能略有不同。如果教程和你的版本对不上优先查官方文档对应版本的说明。根据我个人经验jEasyUI 的 treegrid 在中小后台管理系统里是一个非常实用的组件只要你把数据格式和 idField、treeField 这些基础配置弄扎实后面基本不会出大问题。我在最近一个项目中已经稳定运行了半年多没有出现过严重的缺陷。这个树形网格的用法还可以继续扩展到权限菜单配置、商品类目管理、地区选择等多种场景思路都是相通的。最后再说一个小技巧遇到页面显示异常先不要怀疑组件 bug优先检查接口返回的数据结构和网络状态多数问题都是数据格式不对。
返回列表