ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

jEasyUI Treegrid实战:从数据格式到异步加载与行内编辑全解析

2026/9/19 3:14:05 拓冰建站 浏览量
jEasyUI Treegrid实战:从数据格式到异步加载与行内编辑全解析 做后台管理系统这么多年最绕不开的一个需求就是层级数据展示。部门组织架构、商品类目、菜单权限、地区区划随便拎一个出来都是典型的树形结构。早年我用zTree、用layui的tree后来用jEasyUI做了一整套后台框架发现treegrid这个组件才是真正解决“既要有层级、又要看字段详情”的利器。jEasyUI的树形网格说白了就是把Tree的父子层级关系和DataGrid的列展示、分页、编辑能力合到同一个组件里。创建基础树形网格这件事网上资料不少但大多只贴一段代码没有讲清为什么这么配、数据格式怎么给、踩了哪些坑。这篇博文我就从实际项目角度出发把jEasyUI里创建树形网格的完整流程拆开揉碎讲一遍从基础概念、数据格式到异步加载和行内编辑再到我真实遇到的坑一次性说透。适合正在用jEasyUI做后台、或者刚接触前端组件库的开发者收藏参考。1. 树形网格到底解决什么问题1.1 从DataGrid到Treegrid形态的升级用过jEasyUI的朋友都知道DataGrid处理纯表格数据非常顺手列宽、排序、分页、工具栏一条龙。但DataGrid有个天然缺陷——它处理不了父子层级关系。你想在同一个表格里展示两级的部门列表每一行是一个部门每个部门下又有子部门DataGrid就无能为力了你只能把所有行平铺开然后用“上级ID”这样的字段做隐含关联用户根本看不出层级。Tree组件倒是有层级了但Tree只显示一个节点文本。真实业务哪有这么简单部门要有负责人、要有编制人数、要有创建时间商品类目需要有编码、有排序值、有上架状态。这些附加字段用纯Tree展示要么拼在文本里非常丑要么就得弹窗看详情多了一步操作。treegrid就是为这种场景设计的树的层级骨架表格的列展示能力两者兼得。每一行是一个树节点节点可以展开收缩但每一行又能像普通表格行那样展示多列数据还能直接行内编辑。1.2 为什么选择jEasyUI而不是自研组件前端组件库一抓一大把Element UI、Ant Design都有Table的树形展示为什么还要专门用jEasyUI最实际的原因很多遗留系统的后台管理界面就是基于jEasyUI搭的。你接手一个维护了好几年的项目别人用jEasyUI搭了公共页面框架、封装了增删改查组件你非要为了一个树形表格把Vue全家桶引进来重构成本太高说不定后端接口也得跟着改。与其推倒重来不如顺着原有技术栈把treegrid用熟。另一个原因是jEasyUI的treegrid学习曲线确实很低。整个组件API风格和DataGrid一脉相承只要你会配DataGrid的列再学一个treeField配置和几条父子数据规则基本就能上手。1.3 典型的业务场景我梳理了几个最常见的应用场景遇到这些需求可以直接套用treegrid方案组织架构管理部门表每个部门有名称、负责人、人数、排序上下级通过parentId关联。菜单权限配置菜单表有多级每一行需要显示菜单名称、路由地址、图标、排序、状态。商品类目维护电商后台的类目通常三层起步类目编码、类目名称、佣金比例都要在同一行里展示。行政区划管理省、市、区三级联动每行显示编码和名称还可以带人口、面积这类统计字段。2. 创建树形网格前必懂的数据格式2.1 嵌套JSON结构treegrid和DataGrid最大的区别就在数据源上。DataGrid接受的是通用扁平JSON数组treegrid要求数据本身携带父子关系常见的格式是嵌套JSON[ { id: 1, name: 总公司, principal: 张伟, staffNum: 200, children: [ { id: 11, name: 技术部, principal: 李娜, staffNum: 80, children: [ { id: 111, name: 前端组, principal: 王强, staffNum: 25 }, { id: 112, name: 后端组, principal: 赵敏, staffNum: 30 } ] }, { id: 12, name: 市场部, principal: 刘洋, staffNum: 40 } ] } ]这套数据的核心规则是每个节点的id必须唯一它是父子关系的标识。children字段存放子节点数组没有子节点时可以不写这个字段也可以写成空数组。每行的其它字段name、principal、staffNum是表格要展示的列叫什么名字完全由你的列定义决定。2.2 扁平数据怎么转嵌套数据实际项目里接口返回的往往不是嵌套JSON而是扁平列表每条记录带一个parentId。比如数据库查出来就是[ {id: 1, name: 总公司, parentId: 0}, {id: 11, name: 技术部, parentId: 1}, {id: 111, name: 前端组, parentId: 11}, {id: 12, name: 市场部, parentId: 1} ]这种数据没法直接扔给treegrid需要前端转换。网上转换脚本很多我自己常用的是一个简单的递归函数把扁平结构按parentId组装成children嵌套结构function buildTree(flatData, parentId) { var tree []; for (var i 0; i flatData.length; i) { var node flatData[i]; if (node.parentId parentId) { var children buildTree(flatData, node.id); if (children.length 0) { node.children children; } tree.push(node); } } return tree; } var treeData buildTree(flatData, 0); // 假设顶级节点的parentId是0 $(#tg).treegrid(loadData, treeData);这个函数好理解性能在数据量不大的情况下完全够用。万一数据量非常大建议直接用数组和Map做一次遍历的非递归写法效率更高。但说实话后台系统的部门、菜单数据一般不会超过几百条递归版完全没问题。这里注意一个细节构建的时候如果子节点不存在children字段父节点就不会出现展开箭头这是正常的符合业务预期。2.3 接口返回规范建议结合我多个项目的经验接口返回树形数据时最好让后端直接返回嵌套结构而不是前端每次去转。原因很简单嵌套结构是treegrid的原生格式前端少写转换代码而且后端如果用递归查询或者数据库的递归语法比如Oracle的START WITH CONNECT BY、MySQL 8.0的WITH RECURSIVE产生嵌套JSON非常方便。前端只要在Ajax的success回调里直接loadData就行。如果后端一时半会儿改不了前端转换代码就写成一个公共工具方法挂载到全局对象里所有页面共用别每个页面各写一份。3. 一步一步创建基础树形网格3.1 HTML骨架和列定义创建treegrid的第一种方式是用HTML标记定义一个table然后在th上写列名用data-options声明树形网格的配置!DOCTYPE html html head meta charsetUTF-8 titlejEasyUI 树形网格示例/title link relstylesheet typetext/css hrefjquery-easyui-1.9.4/themes/default/easyui.css link relstylesheet typetext/css hrefjquery-easyui-1.9.4/themes/icon.css script typetext/javascript srcjquery-easyui-1.9.4/jquery.min.js/script script typetext/javascript srcjquery-easyui-1.9.4/jquery.easyui.min.js/script /head body table idtg classeasyui-treegrid stylewidth:100%;height:400px >table idtg2 classeasyui-treegrid stylewidth:100%;height:300px >loadFilter: function(data) { if (data data.data) { return data.data; } return data; }这个技巧在处理统一接口返回格式时特别关键不然你会发现treegrid怎么加载都是空白控制台也不报错排查半天才发现是数据结构对不上。4. 让树形网格交互起来核心事件和API4.1 单击、双击事件处理树形网格能看之外还得能点。jEasyUI的treegrid提供了非常丰富的事件最常用的是onClickRow和onDblClickRow。注意treegrid这里的事件名和DataGrid有相似之处但含义稍有区别DataGrid的onClickRow点击的是任意一行treegrid也一样但点击的行可能是一个有子节点的父节点也可能是一个没有子的叶子节点。业务里经常要根据这一点分开处理$(#tg).treegrid({ onClickRow: function(row) { var isLeaf $(#tg).treegrid(isLeaf, row.id); if (isLeaf) { // 叶子节点跳转到详情页或展示详情 console.log(点击了叶子节点 row.name); } else { console.log(点击了父节点 row.name); } }, onDblClickRow: function(row) { // 双击行例如打开编辑窗口 openEditDialog(row); } });isLeaf是treegrid提供的方法传入节点id返回true表示该节点没有子节点。这个判断在业务中太常用了比如商品类目树只有叶子类目才能挂商品那点击非叶子节点时就要弹提示。4.2 获取选中节点和勾选节点获取当前选中行是treegrid最常见操作用途是配合“编辑”“删除”按钮拿到用户当前操作用的是哪条数据// 获取选中的行单选场景 var row $(#tg).treegrid(getSelected); if (row) { console.log(row.id, row.name, row.principal); } else { $.messager.alert(提示, 请先选择一行数据); }如果你启用了复选框模式在列定义中加checkbox:true那就能用getChecked获取所有勾选的节点var rows $(#tg).treegrid(getChecked); $.each(rows, function(index, row) { console.log(row.id, row.name); });这里有个细节需要特别提醒getChecked默认只返回当前所有勾选的节点不区分层级。如果你只需要叶子节点或者只需要顶级节点需要自己遍历过滤。我看过很多新手直接拿getChecked的结果去批量删除结果把父节点也选上了删的时候外键关联一堆报错。4.3 展开、折叠、刷新操作树形网格的操作总是绕不开这几个方法// 展开某个节点 $(#tg).treegrid(expand, nodeId); // 展开所有节点 $(#tg).treegrid(expandAll); // 折叠某个节点 $(#tg).treegrid(collapse, nodeId); // 折叠所有节点 $(#tg).treegrid(collapseAll); // 刷新当前节点重新加载子节点数据 $(#tg).treegrid(reload, nodeId); // 重新加载整棵树 $(#tg).treegrid(reload);展开和折叠的典型场景页面上放一个“全部展开”“全部折叠”的按钮方便用户在大树里快速定位。刷新节点的场景更常见——在子节点下新增了一条数据你希望树局部刷新而不是整页刷新就调reload传入当前节点id。有一点值得注意reload传入的节点id实际是触发该节点的onBeforeExpand事件或者重新请求该节点下children的url。如果你的树是一次性loadData加载的本地数据reload不传id就是重新拉url。如果你用的是异步加载reload节点id会让该节点重新走一遍子节点加载的逻辑。4.4 新增、删除、修改节点树形网格的增删改做起来也比较顺手核心方法如下// 在指定节点下追加一个子节点parentId为null时追加为根节点 $(#tg).treegrid(append, { parent: parentId, data: [{ id: 999, name: 新部门, principal: 待定, staffNum: 0 }] }); // 删除一个或多个节点 $(#tg).treegrid(remove, [nodeId1, nodeId2]); // 更新某个节点的字段值 $(#tg).treegrid(update, { id: 999, row: { name: 新部门已改名, principal: 钱进 } });append里的parent如果是undefined或者null就是往根节点列表追加。append之后如果父节点之前是折叠状态jEasyUI会自动把父节点展开让新增的节点可见。这个细节我觉得做得挺好的省的自己再手动调一次expand。5. 进阶玩法异步加载和行内编辑5.1 让子节点真正按需加载前面讲的都是把整棵树一次给全。但真实业务中数据量一大一次性渲染几千个节点前端会明显卡顿。这时候应该用异步加载——树初始化时只有根节点用户点击展开节点时才向服务端请求子节点数据。jEasyUI的treegrid支持通过onBeforeExpand事件实现这个能力。基本思路是初始化只加载顶级节点用户在展开某个父节点时组件触发onBeforeExpand在回调里发起Ajax请求拿到子节点数据后用append方法加到当前节点下$(#tg).treegrid({ url: getTopNodes.action, onBeforeExpand: function(row) { var children $(#tg).treegrid(getChildren, row.id); if (children.length 0) { // 已经加载过子节点直接展开不用再请求 return; } $.ajax({ url: getChildNodes.action, data: { parentId: row.id }, dataType: json, success: function(data) { $(#tg).treegrid(append, { parent: row.id, data: data }); $(#tg).treegrid(expand, row.id); } }); } });写到这里必须提醒一个关键点在onBeforeExpand里发Ajax请求展开节点时一定要判断该节点是否已经加载过子节点。否则用户每次折叠再展开都会重复请求子节点数据严重一点会让后端接口压力倍增。getChildren返回的数组长度大于0说明已经加载过了直接return让组件用本地已有的子节点数据展开即可。这个异步加载方案我在实际项目中用了很多次尤其在机构树超过三层、节点数破千时体验比全量加载好太多。另外后端接口返回的子节点数据里最好带上state字段如果是父节点就返回state:closed这样前端才知道这个节点下还有子节点、需要显示展开箭头否则用户点不出子节点会很困惑。5.2 异步加载和loadData不要混用有一个项目因为历史原因初始化时用loadData塞了一部分数据后面又用onBeforeExpand异步拉数据结果出现过一个很隐蔽的问题点击某些节点时子节点被重复append层级乱了。排查后发现loadData加载的数据本身带了children字段组件默认认为该节点已经有子节点onBeforeExpand还是会被触发。我在onBeforeExpand里判断getChildren.length时又没考虑children字段里的初始数据导致重复追加。这里给出一个稳妥的约定使用异步加载时初始化加载的根节点数据里不要写children字段父节点只返回id和name、state等必要字段。所有子节点一律通过onBeforeExpand动态加载。这样getChildren.length的判断才可靠不会误判。5.3 行内编辑直接改不用弹窗行内编辑是treegrid比普通Tree体验好很多的地方。不需要弹出一个表单窗口直接在表格里点一下就能改字段值。treegrid的行内编辑依托的是和DataGrid同款的editor机制。先做一个“开始编辑”的方法var editingId; function editRow() { var row $(#tg).treegrid(getSelected); if (!row) { $.messager.alert(提示, 请先选择一行); return; } // 先把上一次编辑的行结束掉避免多个行同时处于编辑状态 if (editingId) { $(#tg).treegrid(endEdit, editingId); } $(#tg).treegrid(beginEdit, row.id); editingId row.id; }然后在需要编辑的列上配置editor。比如负责人列用文本框编辑器人数列用数字编辑器th>function saveRow() { if (!editingId) { return; } $(#tg).treegrid(endEdit, editingId); var row $(#tg).treegrid(getData, editingId); // row里是最新的行数据可以在这里发Ajax提交到后端 $.ajax({ url: updateNode.action, data: { id: row.id, name: row.name, principal: row.principal }, success: function(res) { $.messager.show({ title: 提示, msg: 保存成功 }); } }); editingId undefined; }这里面有一个API认知上的坑必须单独拎出来讲endEdit之后如果你用getData来获取行数据它拿到的不是treegrid渲染行的实时DOM里的值而是一个内部数据对象。在某些jEasyUI版本里endEdit之后该行数据已经同步了编辑值可以直接用getData取但在另一些版本里你需要用getRows找出这一行然后刷新数据对象。保险起见我一般会在endEdit后通过getData取到行对象如果发现值没更新就手动从DOM里抓值$(#tg).treegrid(endEdit, editingId); var editors $(#tg).treegrid(getEditors, editingId); $.each(editors, function(index, editor) { var field editor.field; var value editor.target.val(); var row $(#tg).treegrid(find, editingId); row[field] value; });这招是我多次踩坑后总结出来的兜底方案特意分享出来。5.4 编辑时注意树形列的联动更新如果编辑的字段正好是treeField指定的列比如部门名称你会发现endEdit之后树的节点文本可能没有立刻刷新。这个是因为treegrid缓存了树节点的显示文本。解决办法是手动刷新一次该节点的行数据$(#tg).treegrid(refreshRow, editingId);refreshRow会重新渲染该行同时更新树的节点文本。如果调完还没有效果可以先移除再追加但那是迫不得已的做法通常refreshRow够用了。6. 实战中那些绕不开的坑6.1 常见问题速查表为了方便大家快速定位问题我整理了一份排查表都是我在真实项目中遇到过的现象根本原因解决办法数据全部平铺没有层级没配idField或treeField在配置里正确设置idField和treeField点击父节点不展开子节点数据里没有state字段或异步加载没触发父节点返回state为closed或检查onBeforeExpand回调展开时报错“node is undefined”id重复或者找不到父节点检查数据中id是否唯一append时parent是否传了正确id编辑后保存的值还是旧值部分版本endEdit后行数据不同步用getEditors手动取输入值并赋值给行对象加载接口后表格空白返回报文结构不是数组loadFilter缺失配置loadFilter抽取data字段中文显示乱码页面或接口编码不一致统一使用UTF-8编码后端设置Content-Type为application/json;charsetUTF-8行内编辑时多行同时编辑没有在beginEdit前endEdit上一行beginEdit之前先判断并endEdit大数据量渲染卡顿一次性加载节点过多改为异步加载展开时才拉取子节点这张表是我平时给团队同学做分享时常用的覆盖面基本能应对90%的新手问题。6.2 性能优化经验树形网格的性能问题我记忆比较深刻。当时做一个地区人口数据管理系统一个省下面有十几个市每个市下面十几个县县下面还有几十个乡镇全量数据接近8000条。刚开始直接一次性加载页面卡得鼠标移动都费劲展开节点时明显顿一下。解决方案是三层优化第一层异步加载。初始只加载省级节点点击省节点时加载市级展开市级时再拉县级这样每次接口只返回几十条数据前端渲染毫无压力。第二层关闭动画。我把animate设为false。节点多的时候动画展开反而让用户感觉更慢直接展开反而干净利落。第三层按需刷新。局部操作完成后调reload传入对应的节点id不整棵树刷新。比如在某市下面新增了一个县就只reload该市节点避免整棵树重新渲染一遍。这套组合拳打下来页面操作回来到了秒开级别用户满意度提升非常明显。6.3 关于对话框里嵌树形网格还有一个常见的复杂场景弹窗里嵌treegrid。比如点击“选择部门”弹出一个对话框里面放一个treegrid。这里有个小坑如果对话框一开始是隐藏的里面放treegrid后直接在页面初始化时渲染会拿不到正确的宽度和高度表格可能挤在一起。解决办法有两种第一种在对话框打开时重新加载并调用resize方法function openDialog() { $(#dlg).dialog(open); $(#tgDialog).treegrid(resize); $(#tgDialog).treegrid(reload); }第二种用dialog的onOpen事件触发resize确保组件在可见状态后重算尺寸$(#dlg).dialog({ onOpen: function() { $(#tgDialog).treegrid(resize); } });这个问题在DataGrid上一样存在本质原因是隐藏容器里组件拿不到最终布局尺寸。记住所有jEasyUI表格类组件放进隐藏容器时都要考虑resize时机。6.4 后端返回的国家标准结构兼容最后再分享一个通用性很强的经验。很多大公司的后端接口返回结构是固定的包装格式类似{code:200, message:success, data:{rows:[...], total:100}}。treegrid用url方式加载时默认会把返回的整个JSON当成节点数组。如果你后端是这种包装格式除了配置loadFilter抽取数据之外如果涉及分页还要注意treegrid本身对分页支持有限。树形网格的分页逻辑和DataGrid不太一样处理起来异常痛苦我的建议是树形网格尽量不要分页改为异步加载节点的方式控制数据量。真要分页就得自己维护每一级节点的分页参数复杂度会指数级上升不划算。7. 写在最后一点个人的使用习惯做前端这么多年用了很多组件库jEasyUI的treegrid是那种“平淡但可靠”的组件。它没有那么多华丽的功能但足够稳定API设计也比较自洽尤其是和jQuery体系的代码混在一起写非常顺手。我现在做后台框架涉及树形结构时还是会优先用treegrid——不是因为它有多高级而是因为团队里接手的人都容易上手维护成本低。如果让我给后来者一个建议那就是动手写之前先把数据格式理清楚。treegrid本身代码不难写大多数人卡住都是卡在数据上结构不对、id不唯一、父id匹配不上、异步时state没返回。把数据层面的规则定明白了后面就是水到渠成的事。最后一句话treegrid不是万能的复杂到一定程度的树交互比如拖拽排序、节点复制粘贴、跨层级移动建议还是用专门的树组件配合定制开发但基础的后台管理需求jEasyUI treegrid完全够用而且能省下不少时间。