news 2026/9/23 4:49:37

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 charset="utf-8"> <title>jEasyUI 基础树形网格</title> <link rel="stylesheet" type="text/css" href="themes/default/easyui.css"> <link rel="stylesheet" type="text/css" href="themes/icon.css"> <script type="text/javascript" src="jquery.min.js"></script> <script type="text/javascript" src="jquery.easyui.min.js"></script> </head> <body> <table id="treeTable"></table> </body> </html>

需要注意,jEasyUI 对 jQuery 的版本有一定要求,我用的组合是 jQuery 1.11.3 搭配 jEasyUI 1.9.x,稳定性很好。建议别直接上 jQuery 3.x 搭配老版本 easyui,容易出现兼容性报错。

2.2 表格容器与初始化

初始化 treegrid 有两种常见方式。第一种是直接在 table 标签上写><table id="treeTable" class="easyui-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="#" onclick="editNode(\'' + row.id + '\')">编辑</a>'; var delBtn = ''; if (row.status !== '停用') { delBtn = '<a href="#" onclick="deleteNode(\'' + row.id + '\')">删除</a>'; } return editBtn + ' ' + delBtn; }} ]] });

在事件处理函数里,注意使用事件对象的 stopPropagation,防止点击按钮时触发整行的选中事件。我习惯把行 id 传到函数里,再通过 treegrid 的 getData 方法拿到整行数据。

4. 完整案例:部门组织架构树形网格

理论说太多不如直接做一个完整案例。这一节我把一个部门组织架构管理页面的核心代码完整展示出来,包含需求分析、前后端数据交互和前端实现。

4.1 需求梳理与数据设计

假设我们要做一个简单的部门管理页面,包含如下需求:

  • 展示部门层级关系,每一行显示部门名称、部门编码、负责人、人数、状态。
  • 顶级部门默认展开一层。
  • 点击操作列中的“查看成员”按钮,弹出窗口展示该部门下的人员列表。
  • 支持折叠和展开动画。

后端接口约定如下:

  • GET /api/dept/tree:返回整棵部门树。
  • GET /api/dept/members?deptId=1:返回指定部门下的人员列表。

返回的部门树数据格式:

[ { "id": 1, "deptName": "总裁办", "deptCode": "D001", "manager": "张总", "memberCount": 12, "status": "启用", "children": [ { "id": 2, "deptName": "行政部", "deptCode": "D00101", "manager": "李主任", "memberCount": 8, "status": "启用", "children": [] } ] } ]

4.2 前端页面完整代码

页面结构分为两部分:左侧一个树形网格,右侧一个用于展示成员信息的面板。这样页面信息密度高,又不会互相干扰。

<div style="padding: 10px;"> <table id="deptTree"></table> </div> <div id="memberPanel" style="display: none;"> <table id="memberGrid"></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="#" class="viewMemberBtn">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 在加载失败时通常只是不显示数据,控制台不一定有红色错误。排查步骤我总结为四步:

  1. 打开浏览器的 Network,确认请求是否发出。
  2. 查看响应状态码是否为 200。
  3. 查看响应内容是否为合法 JSON。
  4. 检查 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,优先检查接口返回的数据结构和网络状态,多数问题都是数据格式不对。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 4:49:22

《一年顶十年》核心解读:牛人、贵人、团队如何形成成长正循环

一开始我以为是“速度”&#xff0c;后来发现是“换引擎”提到《一年顶十年》这个书名&#xff0c;很多人第一反应是“又一本打鸡血的成功学”。我最初翻开它的时候&#xff0c;也是带着这种偏见——市面上教你“弯道超车”的书实在太多了&#xff0c;多到让人觉得“快”本身就…

作者头像 李华
网站建设 2026/9/23 4:48:35

移动应用门户架构设计:统一认证、Saga与CQRS实战解析

1. 为什么做移动应用门户&#xff1a;先想清楚要解决什么问题1.1 企业移动应用的真实痛点这两年我接触了不少做企业数字化转型的团队&#xff0c;发现大家都会走到同一个岔路口&#xff1a;内部系统越上越多&#xff0c;OA、审批、考勤、CRM、工单、报表、培训考试……每个系统…

作者头像 李华
网站建设 2026/9/23 4:46:33

从倒排索引到AI流式输出:搜索引擎技术栈全链路拆解

很多做技术朋友问我&#xff0c;想理解头部搜索引擎到底是怎么工作的&#xff0c;最简单的切入点是什么。我通常会反问一句&#xff1a;你上一次在百度搜索框里敲下关键词、按下回车&#xff0c;到结果页完全呈现&#xff0c;中间到底发生了什么&#xff1f;大部分人会愣一下&a…

作者头像 李华
网站建设 2026/9/23 4:44:42

图像分割实战指南:语义、实例与全景分割从训练到部署

简介&#xff1a;一份覆盖语义分割、实例分割与全景分割的图像分割Python项目&#xff0c;适合计算机视觉与深度学习方向的开发者、学生及科研人员&#xff0c;可直接用于毕设或课程设计参考。压缩包共5个文件&#xff0c;含3个Markdown说明文档、1个Python脚本与1个工程文件&a…

作者头像 李华