简介:Bootstrap TreeGrid 插件基于 Bootstrap 框架,面向 Web 前端开发者,主要解决表格内层级数据的展示与交互问题,常用于权限管理、分类目录等需要嵌套结构的页面,也适用于后台管理系统或内容组织场景。压缩包共 7 个文件,包含 3 个 JavaScript 插件文件(实现树形数据加载、展开折叠、事件处理等核心逻辑)、2 个 CSS 样式文件(用于控制节点缩进与外观风格)以及 2 张 PNG 示例图片(方便查看效果预览),整体仅 33 KB,非常轻量,可直接集成到现有 Bootstrap 项目中。目前已有 380 人学习/下载,适合具备基础 Bootstrap 使用经验的开发者快速上手。资源附带演示页面和使用说明,可帮助读者掌握 TreeGrid 的初始化配置、数据源绑定、节点状态控制等关键步骤;同时它支持自定义扩展列、排序搜索和 API 接口调用,可以直接在项目中复用,提升后台管理界面的信息组织效率与用户体验。
1. 树形表格插件到底在解决什么问题:从部门列表到文件目录的同一套痛点
做后台管理系统时大概率会遇到一个需求:左侧明明有一棵组织架构树,右侧的表格却只能平铺展示所有部门,上下级关系全靠"上级部门"这一列的文字硬撑。当产品经理提出"部门列表要能按层级展开收起,同时每一行还要有负责人、人数、操作按钮"时,普通表格拉不平,树组件又丢了表头对齐。最后落地的方案基本都指向同一个方向:基于 Bootstrap 的树形表格 js 插件,在表格行上做层级缩进和展开收起,同时保留列操作能力。这篇文章从选型原理、最小可运行代码讲到高频踩坑和进阶技巧,照着做就能在项目里跑起来。
2. 选型与渲染原理:搞懂插件是怎么把树画进表格里的
2.1 三种主流路线:改造表格、树形组件拼接、现成插件
先说我实际见过的三种做法。第一种是手写逻辑,在普通表格里加一个层级列,用字符串拼接缩进空格,再用一个数组记录每个节点的展开状态。层级固定两层以内的时候很清爽,一旦出现第三层,缩进计算和状态联动开始失控,遇到增删改节点时大量代码要重写。第二种是树组件加表格拼起来,左边一棵树当导航,右边表格显示当前节点数据,视觉上没问题,但用户会追问:树里选中的节点为什么表格里没有高亮,表格的筛选条件和树的状态为什么不同步。这种联动本质上是两套状态硬凑,维护成本随需求增加线性膨胀。
第三种是直接用现成的 treegrid 插件,这也是大多数项目最终走的路。这类插件基于 jQuery 加 Bootstrap 的表格能力,初始化时传入树形数据或平铺数据,自动生成缩进和展开箭头。因为被大量项目跑过,主要坑位都浮出来了,反而是最好落地的选择。除非项目明确要求零 jQuery 依赖,否则我一般会直接选这条路。
三种路线做个对比:
| 路线 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 表格手写缩进 | 零依赖、完全可控 | 状态管理繁琐,层级深了容易乱 | 层级固定的两级小表 |
| 树组件加表格 | 树交互体验完整 | 两套数据状态割裂,联动代码工作量大 | 树做导航、表格只读 |
| 现成 treegrid 插件 | 开箱即用、方案成熟 | 依赖 jQuery,深度定制受插件边界限制 | 大多数后台管理端场景 |
有一点容易被忽略:现成插件不等于零成本。插件默认样式依赖 Bootstrap 的表格类名,整体引入 jQuery 在老项目里没什么负担,但如果项目已经迁到 Vue 或 React,塞一个 jQuery 插件会显得很重。这种时候更要慎重,新项目优先找组件库自带的树表格,老项目维护才考虑插件。
2.2 树形数据格式:parentId 平铺与嵌套 children
树形表格的输入数据有两种常见组织方式,后端接口大多数返回平铺格式,前端组件大多数偏爱嵌套格式。先把两者的区别看清,后面所有转换逻辑都从这里出发。
// 方式一:平铺加 parentId 关联,后端返回最常见 const flatData = [ { id: 1, parentId: 0, name: '总部', leader: '张明' }, { id: 2, parentId: 1, name: '研发部', leader: '李华' }, { id: 3, parentId: 1, name: '市场部', leader: '王芳' }, { id: 4, parentId: 2, name: '前端组', leader: '赵磊' }, { id: 5, parentId: 4, name: '基础架构组', leader: '钱进' } ]; // 方式二:嵌套 children,前端组件更常用 const nestedData = [ { id: 1, parentId: 0, name: '总部', leader: '张明', children: [ { id: 2, parentId: 1, name: '研发部', leader: '李华', children: [ { id: 4, parentId: 2, name: '前端组', leader: '赵磊' } ] }, { id: 3, parentId: 1, name: '市场部', leader: '王芳' } ] } ];平铺格式的优点是构造简单,数据库里也好存,直接按 parentId 分组就能组装关系,缺点是层级关系藏在一串 id 里,肉眼不容易看出结构。嵌套格式正好相反,层级一目了然,但要递归读取,后端拼装也麻烦一些。
大部分 treegrid 插件内部最终都要把数据展平成带层级标记的行数组,所以两种格式可以互换。平铺转嵌套的常见做法是先按 parentId 建索引再递归组装,嵌套转平铺用深度优先遍历,每层记录深度。不需要把每种写法都背下来,但必须知道插件初始化的参数里会说明它接收哪种格式,传错格式是新手最容易翻车的地方。
2.3 渲染原理:一次展平加状态标记
理解了数据格式,再看插件怎么把树画出来。几乎所有 treegrid 插件的主线逻辑都一样:把嵌套结构展平成一维行数组,给每行标注层级和可见性。渲染时表格只渲染可见行为 true 的行,折叠一个节点时,把它所有后代行标记为不可见。
展开状态一般用集合维护,比如 expandedIds。每次点击箭头,插件在这个集合里增删节点 id,重新计算全部行的可见性,再局部更新表格。这个机制决定了两件事:展开、折叠操作本质上是整表重算,数据量大时会卡;排序、分页这类扩展如果挂在表格原有逻辑上,容易打破可见性的一致性,第 4 章会展开讲。
层级缩进也不是靠加空格实现,而是通过给每行插入带 padding-left 的占位单元格,或动态设置第一格的内边距。这解释了为什么自定义列宽时,第一列的缩进表现有时候会显得奇怪。
3. 跑通第一个树形表格:依赖、HTML 与初始化配置
3.1 最小依赖清单和引入顺序
先确认项目里是否已有 jQuery 和 Bootstrap,大多数老项目都自带,缺的话要补上。依赖按顺序引入,顺序错乱会出现各种"插件没生效"的玄学问题,先给最小组合:
<link rel="stylesheet" href="bootstrap.min.css"> <link rel="stylesheet" href="treegrid.css"> <script src="jquery.min.js"></script> <script src="bootstrap.min.js"></script> <script src="treegrid.js"></script>样式部分先加载 Bootstrap 的 bootstrap.min.css,再加载 treegrid 的 css,后者的展开箭头和缩进样式要在 Bootstrap 之后生效。脚本部分必须 jQuery 在前,Bootstrap 的 js 其次,最后才是 treegrid 插件脚本,因为插件初始化时依赖前两者的全局对象。
注意不要图省事把 treegrid 的 css 和 js 合并到一个自定义文件里,插件内部可能依赖 Bootstrap 的类名,比如 .table、.collapse,合并压缩时顺序一乱,排查起来非常难受。
3.2 最小可运行页面:HTML 结构加数据源
下面这个页面是验证插件能不能用的最小骨架。它不涉及业务逻辑,只验证"数据进来、树形渲染、展开收起正常":
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="bootstrap.min.css"> <link rel="stylesheet" href="treegrid.css"> <script src="jquery.min.js"></script> <script src="bootstrap.min.js"></script> <script src="treegrid.js"></script> </head> <body> <div class="container"> <table id="deptTable" class="table table-bordered"> <thead> <tr> <th>部门名称</th> <th>负责人</th> <th>人数</th> </tr> </thead> <tbody></tbody> </table> </div> <script> const flatData = [ { id: 1, parentId: 0, name: '总部', leader: '张明', count: 12 }, { id: 2, parentId: 1, name: '研发部', leader: '李华', count: 80 }, { id: 3, parentId: 1, name: '市场部', leader: '王芳', count: 30 }, { id: 4, parentId: 2, name: '前端组', leader: '赵磊', count: 15 }, { id: 5, parentId: 4, name: '基础架构组', leader: '钱进', count: 6 } ]; $('#deptTable').treegrid({ data: flatData, idField: 'id', parentField: 'parentId', treeColumn: 'name', expandAll: false }); </script> </body> </html>这段代码的逻辑:插件收到平铺数据后,根据 idField 和 parentField 找出父子关联,由 treeColumn 指定的字段所在的列渲染树形缩进和箭头。expandAll 传 false,让根层级初始展开、下层默认收起,页面加载后只看到总部及研发部、市场部三个根节点。
几个参数值得单独说。treeColumn 必须对应表头里真实存在的列名,传错的话缩进和箭头会跑到别的列上。expandAll 在数据量几十行时随便用,换成几千行它就是卡顿的第一嫌疑。idField 不要省略,即使数据里没有 id 字段,也建议在接口层做一次映射,因为插件的展开状态集合依赖它做 key。
3.3 初始化配置与必调参数:从 treeColumn 到 onClickRow
跑通最小示例后,最常改的配置集中在这几个参数上。做项目前先抄一遍这个表,心里有底:
| 参数名 | 作用 | 推荐值 |
|---|---|---|
| idField | 节点唯一标识字段 | 数据里的主键名 |
| parentField | 父节点标识字段 | 平铺数据必须指定 |
| treeColumn | 树形列,决定缩进渲染到哪一列 | 选层级信息所在列 |
| expandAll | 是否初始化全部展开 | 数据少于 200 行才建议 true |
| onChange | 展开收起后的回调 | 用于同步其他组件状态 |
| onClickRow | 点击行回调 | 用于行选中或详情跳转 |
再给一段带事件的初始化代码,对后面做联动很有用:
$('#deptTable').treegrid({ data: flatData, idField: 'id', parentField: 'parentId', treeColumn: 'name', expandAll: false, onChange: function (row, expanded) { // 当节点被展开或收起时触发,row 是当前行数据 refreshChildCount(row.id, expanded); }, onClickRow: function (rowEle, rowData) { // 用户点击任意行时触发,常用于加载详情 loadDeptDetail(rowData.id); } });onChange 的回调签名在不同实现里略有差异,常见是传入当前行数据和展开状态布尔值。onClickRow 要注意它是点击任何一行都触发,如果想让点击箭头和点击行本身的行为区分开,我一般会把箭头区域的点击单独绑定,避免误触发。
4. 树形表格 5 个高频踩坑现场:现象、原因、解决
这一章是血泪经验,每条都来自实际项目。提前看完,比测试报告丢过来再查高效得多。
4.1 展开图标不显示,点击也没有反应
现象:表格正常渲染,层级缩进也有,唯独每行前面的箭头是空白,点击也没反应。
原因:treegrid 样式文件用了一种字体图标,引入 treegrid.css 后,如果页面里没有对应字体文件,或者字体路径被构建工具改写错,图标就渲染成空白。更隐蔽的是部分插件把图标写在伪元素 content 里,浏览器控制台不报错,表现为"一切正常但就是没图标"。
解决:打开网络面板看字体请求是否 404。如果 404,把字体文件放到正确路径,或者用 CSS 覆盖图标为文本符号,我常用的方案是覆盖成+和-:
.treegrid-expander { font-family: Arial, sans-serif; } .treegrid-collapse::before { content: '-'; } .treegrid-expand::before { content: '+'; }这个方案不依赖字体文件,任何构建环境下都不会再出现"图标消失"的问题,缺点是样式朴素,但对后台系统完全够用。
4.2 排序功能把树形层级打乱
现象:给表格开启排序,点击表头按人数排序,父子关系乱了,子级跑到父级上面,或者根节点和子节点混在一起。
原因:树形表格的排序扩展直接对渲染用的展平数组排序,它不知道哪一行是谁的子节点。只要排序逻辑没约束"父节点必须先于子节点",结果必然错乱。
解决:如果业务必须要排序,我一般会禁止全表排序,改为列内排序。在排序回调里先按根节点分组,组内再排子节点,层级就不会被打乱:
$('#deptTable').treegrid({ // 禁用插件自带的全表排序 sortable: false }); // 自己实现列内排序:取出当前展开的所有行, // 按 parentId 分组,组内排序后重新写入表格 function sortVisibleRows(groupKey) { const rows = getAllVisibleRows(); const groups = groupByParentId(rows); Object.keys(groups).forEach(pid => { groups[pid].sort((a, b) => a[groupKey] - b[groupKey]); }); renderSortedRows(groups); }更省事的做法:如果排序需求不强烈,直接关闭排序功能,成本最低。排序和树形结构天然有冲突,接需求时先确认是不是又要做一个互相矛盾的交互。
4.3 分页后展开状态丢失
现象:展开某个根节点的三级子级,翻到下一页再翻回来,发现所有节点都收起了。
原因:分页扩展重新渲染时,只把当前页数据交给表格,展开状态的集合没有和页面绑定,翻页等于重新初始化。
解决:加一个全局展开状态缓存,在翻页前保存当前展开节点集合,表格渲染完成后恢复:
let expandedCache = new Set(); // 在每次翻页前调用,收集当前展开行 function beforePageChange() { expandedCache.clear(); $('#deptTable').find('tr.treegrid-expanded').each(function () { expandedCache.add($(this).data('id')); }); } // 在表格重新渲染完成后调用 function afterPageRender() { expandedCache.forEach(function (rowId) { $('#deptTable').treegrid('expandRow', rowId); }); }这个方案只对展开状态有效,折叠状态不需要缓存,因为新增行默认都是收起。还有一个边界要留意:如果分页接口本身是懒加载子节点,翻页回来子节点数据可能还没到,需要在数据加载完成后再做一次恢复展开的判断。
4.4 懒加载子节点后表格行高跳动
现象:点击一个根节点展开,子节点数据从接口返回后,表格行高突然变化,滚动条位置跳动,体验很突兀。
原因:懒加载是先渲染一个占位行,数据回来后把占位行替换成真正内容。占位行高度和真实行不一致,替换时表格触发重排。
解决:让占位行高度与正常行保持一致,给占位行设置固定最小高度。更简单的方案是展开时不立刻渲染占位行,而是先显示一个"加载中"文本行,数据回来后替换,有加载过程的视觉过渡,跳动感会明显减弱。
4.5 一次性展开几千行导致页面卡死
现象:接口一次返回三千行数据,初始化配置里 expandAll 为 true,页面白屏几秒甚至弹出"无响应"提示。
原因:插件把几千行所有单元格的 DOM 一次性创建,还要做一次性可见性计算,数据量大时性能急剧下探。
解决:业务层做三层策略。第一,接口分页,每页最多五百行,注意和分页共存时用 4.3 节的缓存方案,且只在单页内有效,跨页缓存会冲突。第二,默认只展开第一层,用 expandLevel 之类的参数代替 expandAll。第三,如果数据是组织架构这种层级结构,让接口先返回根节点框架,子节点懒加载,页面永远只渲染当前展开的节点,这是最可靠的做法。
5. 进阶:让树形表格能落地生产的四个技巧
5.1 自定义缩进、图标和初始展开层级
默认样式往往和项目 UI 风格不搭,最常见的需求是改箭头图标、缩进宽度和初始展开层级。缩进宽度直接改 CSS 的 padding-left:
.treegrid-indent { width: 20px; /* 每层缩进宽度,默认16px,层级多时调小 */ } .treegrid-expander { width: 20px; /* 箭头区域宽度 */ cursor: pointer; color: #2f6fed; /* 箭头颜色 */ }初始展开层级这个需求,大多数插件没有现成参数。我的做法是初始化后手动展开需要的行,而不是依赖配置:
$('#deptTable').treegrid({ expandAll: false }); // 数据加载完成后,手动展开指定节点 $('#deptTable').treegrid('expandRow', 1);这里的逻辑是 expandRow 接收节点 id,插件会展开它下方的子级。用这个方式可以不改数据源,实现"默认展开前两层"或"默认展开某个部门"这类业务诉求。
5.2 选中父节点联动子节点
这是权限管理里最常见的需求:勾选一个父节点,它所有子节点也被选中;取消父节点,子节点全部取消。插件一般只提供单行选择,联动逻辑要在点击回调里自己实现:
function toggleChildren(rowData, checked, rows) { rows.forEach(function (row) { if (row.parentId === rowData.id) { setRowChecked(row.id, checked); // 递归处理下级,这里用 parentId 判断 if (row.children && row.children.length > 0) { toggleChildren(row, checked, rows); } } }); }注意这里拿到的 rows 是插件内部的展平数据,不是原始嵌套数据,所以递归判断用 parentId 而不是 children。这个细节不对,联动就只生效一层,排查时还看不出来。
实际项目中子节点选中还受半选状态影响,即父节点的部分子节点被选中时,父节点应显示半选图标。插件不一定支持半选,我会在父节点回调里统计子节点选中数,手动切换父节点的样式类。
5.3 懒加载与服务器端数据获取
接口返回整棵树在层级深时不可接受,懒加载是必选项。做法是:初始化只传根节点列表,展开节点时在 onChange 里判断该行是否还有未加载的子节点,有则发请求,返回后调用插件的追加子节点方法:
$('#deptTable').treegrid({ data: rootList, idField: 'id', parentField: 'parentId', treeColumn: 'name', onChange: function (row, expanded) { if (expanded && row.hasChildren && !row.childrenLoaded) { $.ajax({ url: '/api/dept/children', method: 'GET', data: { id: row.id }, success: function (children) { // 把返回的子节点插入当前行下面 $('#deptTable').treegrid('appendChild', { parentId: row.id, children: children }); } }); } } });这个方案的边界要特别注意:用户快速连续点开、收起、再点开同一个节点,会触发多次请求,后一次返回会把前一次的数据重复追加。我在项目里用一个请求去重 Map,key 是节点 id,请求在途时直接忽略新触发,这是典型的"低概率但必现"的坑。
5.4 增删改后的局部刷新与状态恢复
增删改节点后最忌讳的是整个表格重新初始化,那会把用户展开状态全丢,还带来一次全量渲染。正确做法是局部操作,再保存展开状态:
// 删除节点:先保存展开状态,再移除当前行及其后代 function removeDept(rowId) { const expanded = collectExpandedIds(); $('#deptTable').treegrid('removeRow', rowId); restoreExpanded(expanded); } // 新增节点:直接追加到指定父节点下 $('#deptTable').treegrid('appendChild', { parentId: parentId, children: [newRow] });新增时要注意,appendChild 如果父节点当前是收起状态,新节点在视觉上不可见,但数据已在内部数组里。用户下次展开时会看到数据,如果后端同步返回时又做了一次全量刷新,就可能出现"新增了一次却出现两条"的问题。处理办法是新增后立即把父节点展开,或者调用刷新节点方法重读该父节点下的数据。
6. 收尾:验证树形表格健壮性的一个笨办法
项目后期最常翻车的不是功能跑不起来,而是各种操作组合之后的状态错乱。分享一个我一直在用的笨办法:状态矩阵验证法。手工构造一个包含 3 层、每层 3 个节点的测试数据,按「先展开某层 → 收起 → 翻页 → 再展开 → 刷新 → 排序」的顺序操作一遍,每做一步就打印当前所有可见行的 id 顺序,和期望结果对比。
打印逻辑很简单:遍历表格当前可见行,把每行 id 和层级输出到控制台,人工核对是否符合展开收起预期。这个方法不依赖测试框架,5 分钟能完成一轮,但能把插件内部状态有没有错乱看得清清楚楚。
我第一次用这个办法是在某权限管理项目上线前夜,当时只测了"加载后默认展开叶子节点"的流程,没测"先展开再刷新"的路径。结果线上用户按「展开部门 → 搜索成员 → 清空搜索」操作了一遍,树全部收起,用户以为系统坏了。后来用状态矩阵跑了一遍才发现,刷新操作把展开状态集合清了,但没重新加载,修复方案就是在刷新后调一次恢复展开的接口,问题当场消失。
现在每接一个树形表格需求,我都会先花 10 分钟把这个验证流程跑一遍再交测试。行顺序、展开层级、翻页状态这三个点,是树形表格最脆弱的地方,先确认它们稳了再上业务功能。希望帮到你。
本文还有配套的精品资源,点击获取