简介:Bootstrap TreeGrid是一款基于Bootstrap框架的树形表格JavaScript插件,用于在后台管理、权限配置等场景中清晰展示多级嵌套数据。它内置节点折叠展开、事件监听、样式自定义和数据绑定能力,支持通过JSON或AJAX动态加载内容,可有效提升层级信息的浏览与操作效率,同时提供API方法和事件回调,便于深度定制交互行为。压缩包体积仅33KB,共7个文件,包含3个JS脚本、2个CSS样式表和2张PNG图片,结构精简,适合直接集成到基于Bootstrap的Web项目中。目前已有380人学习,适合具备一定前端基础、需要快速实现树形表格交互的开发者。解压后可获得核心插件脚本、配套样式及示意图片,能够帮助理解初始化参数、展开/折叠逻辑和表格样式定制方式,从而缩短上手周期并灵活扩展出符合业务需求的层级展示效果,无论是快速原型还是正式功能模块均可直接复用。
1. 树形表格到底解决什么问题:为什么普通表格撑不住层级数据
做后台管理系统做到第三个月,我就被一个典型需求卡住了:省级-市级-区县三级联动菜单,分类-子分类-商品列表,或者组织架构里的部门-小组-成员。用普通表格渲染,要么把所有层级拍扁成一行行平铺,靠缩进猜父子关系;要么拆成好几个页面跳来跳去,用户每看一层就要点一次返回。这两种方式我在某公司的项目里都试过,最后都被产品打回。树形表格(TreeGrid)就是干这个的:它把“树”的展开折叠交互和“表格”的字段排序、固定列、操作按钮结合起来,让用户在同一张表格里既看到完整层级,又能按需展开某一支。
这个标题里的“Bootstrap树形表格JS插件”,本质上是给Bootstrap生态补齐一个高阶表格能力。适合谁?适合正在用Bootstrap 3/4做后台、被层级数据展示困扰、又不想引入Vue或React重写前端的开发者。它解决的不是“能不能显示树”,而是“在已有的Bootstrap风格下,用最小改动获得树形展开、收起、按需加载的完整交互”。下面这套做法我前后用了大半年,几种常见插件都折腾过,最后沉淀下来一套能直接复制的选型和落地思路,新手跟着走能跑通,熟手可以跳过基础直接看参数和踩坑部分。
2. 选型:先搞清楚你是要“树”还是“表格”,再去挑插件
2.1 三种实现路径与适用边界
拿到类似需求,普遍会有三条路可选。第一条是给Bootstrap Table挂树形扩展,这是标题指向的最常见做法,在Bootstrap表格基础上增加父子行折叠能力。第二条是用独立的TreeGrid插件,这类插件不依赖Bootstrap Table,只复用Bootstrap的样式类名。第三条是自己用递归组件写,对于只用过一次、以后不再复用的场景,反而更可控。
我做过一个项目X,数据量只有几百行,当时图省事直接上了自研递归渲染。用jQuery拼字符串递归生成行,展开折叠全靠手动维护展开状态数组。做到第三周就发现问题:排序、翻页后展开状态要重新计算,勾选父节点要联动子节点,这套逻辑越写越厚,最后代码量不比插件少,还不敢轻易改。所以我的建议是:凡是后续还要加“展开时请求子节点”“父子联动选中”“导出带层级”这类需求,就别走自研路线,直接用成熟插件。只有一种情况自研更合适——你要的只是纯展示,不允许交互,而且层级深度固定不超过两层。
Bootstrap Table树形扩展这套组合,适合数据行数在几千以内、已经用Bootstrap Table做列表、希望尽量少改原有初始化逻辑的项目。如果项目还没用Bootstrap Table,可以先引入它。因为树形扩展本质上是在Bootstrap Table的渲染结果上做二次加工,它要求你先有一个能正常工作的Bootstrap Table基础表格。
2.2 我关注的选型清单:展开方式、数据量、依赖体积
选插件之前,我会先列一个清单而不是直接下载。第一项是看它支持的数据格式是嵌套还是扁平。嵌套格式就是每个节点对象里挂children数组,结构清晰,但后端如果返回的是数据库里的父子两列结构,你得先转换;扁平格式用parentId关联,和后端表结构天然匹配,但插件初始化时要额外做一次树形化。两种都支持的最好。
第二项是展开方式。有的插件是整行点击展开,有的必须点行首的箭头图标。这直接影响移动端体验,如果你的用户会在平板上操作,整行可点比“精准点中那个小箭头”友好得多。第三项是异步加载能力。真正生产环境,树形表格最怕的就是一次把全量数据塞给前端——组织架构三层全展开可能有上万行,页面直接卡死。插件如果支持父节点展开时才请求子节点,这个才是能扛生产压力的功能。具体到Bootstrap Table的树形扩展,常见做法是配置一个子节点请求URL,展开某行时自动带着该行主键请求子数据。
依赖体积要放在最后看,但也不能不看。整套方案是引入Bootstrap、jQuery、Bootstrap Table三个基础依赖之后,再叠加树形扩展和图标字体库。在后台系统里这些体积基本可接受,但如果你的页面是面向C端用户的,加载多出来的一两百KB指标要慎重。后台项目我一般不计较这点体积,C端项目我会优先试独立TreeGrid插件,依赖更少。
2.3 一个最小验证Demo:先确认插件方案的可行性
选型阶段不需要把完整功能做完,我习惯先花半天时间搭一个最小的验证Demo。目的只有一个:确认插件的初始化方式和你当前的Bootstrap版本能正常共存。Bootstrap 3系列的表格皮肤和Bootstrap 4、5之间的类名差异很大,有些树形插件绑定的是旧版类名,直接搬过来会出现样式错位、边框对不上。
这一步要做的就三件事:引入插件静态文件、准备一份写死的三级JSON数据、初始化一个开启树形模式的表格。如果这个Demo能在浏览器里展开和折叠,基础链路就算通了。我见过太多人选型时只看文档截图,真正集成时发现插件停止维护了、和当前Bootstrap版本不兼容,被迫中途换方案。先跑Demo,是最省时间的一步。
提示:选型Demo里不要用后端真实数据,用写死在JS里的JSON即可,否则会把“插件问题”和“接口问题”混在一起,排查时反复横跳。
3. 把最小可用版跑起来:HTML结构、初始化脚本与数据格式
3.1 页面引入与依赖顺序
我通常会在页面底部按固定顺序引入脚本:jQuery在最前,Bootstrap的JS紧随其后,然后是Bootstrap Table,最后才是树形扩展。顺序反了会出现“插件注册不到Bootstrap Table上”的报错,表现是表格正常渲染但还是平铺,没有任何树形交互。这个顺序不是玄学,因为树形扩展内部是通过Bootstrap Table的扩展点注册的,必须先让Bootstrap Table被加载完毕。
// 这段是静态资源引入顺序,新增树形能力时不要打乱 document.write('<script src="/libs/jquery.min.js"></script>'); document.write('<script src="/libs/bootstrap.min.js"></script>'); document.write('<script src="/libs/bootstrap-table.min.js"></script>'); document.write('<script src="/libs/bootstrap-table-treegrid.min.js"></script>');我一般不建议用document.write,这里只是为了强调顺序。正常项目里直接写<script>标签也行,关键就是最后一行必须晚于第三行。CSS也是同理,Bootstrap主样式先引入,树形扩展的补充样式作为增强放在后面,这样图标和展开缩进才能覆盖上去。如果你的项目里已经有Bootstrap Table在跑,把这个脚本顺序核对一遍就行——多数人翻车都是因为树形扩展的JS被某个合并压缩工具提前执行了。
参数说明里再提一句:此处树形扩展的文件名是示意,实际从开源仓库下载的文件名以项目为准。文件名不重要,重要的是加载顺序。
3.2 初始化Bootstrap Table并开启树形模式
数据用一个带层级关系的JSON数组来演示。这里我采用扁平结构,因为后端接口最容易返回这种结构,前端做一次转换也不难。
// 模拟后端的扁平数据:每条记录都带有parentId const rows = [ { id: 1, parentId: 0, name: '华东大区', count: 3 }, { id: 2, parentId: 1, name: '上海分部', count: 2 }, { id: 3, parentId: 2, name: '浦东组', count: 5 }, { id: 4, parentId: 1, name: '杭州分部', count: 2 } ]; $('#treeTable').bootstrapTable({ data: rows, treeView: true, // 开启树形模式 treeShowField: 'name', // 指定哪一列显示树形缩进和箭头 idField: 'id', // 主键字段 parentIdField: 'parentId', // 父节点字段 onExpandRow: function(index, row) { // 行展开时触发,可用于异步加载子节点 console.log('展开节点:', row.name); } });这段就是最小可用的树形表格初始化。treeView: true是总开关,缺了它,后面的字段配置全部无效。treeShowField必须填写你的业务字段,插件靠它决定在表格哪一列绘制展开箭头和层级缩进,如果不设置,很多插件的默认行为是自动找第一列,如果你的第一列是“序号”,树状缩进就会长在序号列上,视觉非常奇怪。idField和parentIdField是插件把扁平数据组装成树的关键,这两个字段必须对应后端返回的JSON字段名,不能是数据库里的字段名。
3.3 树形数据格式与默认展开配置
扁平数据交给插件后,有的插件会自动做树化,有的不会。为了少踩坑,我一般会自己先把数据转换成嵌套格式,再交给表格,这样不依赖插件内部的实现差异。转换逻辑不复杂,用一个Map先按id索引,再遍历一次挂到对应的children里。
function buildTree(rows) { const map = new Map(); const tree = []; rows.forEach(row => map.set(row.id, { ...row, children: [] })); rows.forEach(row => { if (row.parentId && map.has(row.parentId)) { map.get(row.parentId).children.push(map.get(row.id)); } else { tree.push(map.get(row.id)); // 没有有效父节点的,当作顶级 } }); return tree; } $('#treeTable').bootstrapTable({ data: buildTree(rows), treeView: true, treeShowField: 'name' });为什么要自己转换而不是让插件转?因为后端返回的parentId有时候是字符串“0”,有时候是数字0,有时候根节点压根没有parentId字段。这些脏数据交给我们自己的转换函数,至少能用一个统一的规则兜底:找不到父节点的,一律挂在顶级。这样即使后端偶尔漏传字段,页面不会突然展开异常。另一个好处是转换时可以顺手为每个节点补一个_level字段,后续用来自定义缩进和样式。
参数说明:buildTree是纯前端逻辑,不依赖插件内部实现,所以任何品牌要换成别的插件,这段数据加工代码可以原样复用。这也是我推荐的实践——数据预处理永远放在业务层,不要依赖某个插件替你处理脏数据。
4. 常用功能与参数:展开折叠、异步加载、分页筛选到底怎么调
4.1 展开/折叠相关的必调参数
树形表格最核心的交互就是展开折叠,但真正用过之后会发现,默认行为常常不是你想要的那个。常见做法是初始化后所有节点默认展开,数据一多页面直接拉得很长;或者默认全部收起,用户需要一层层点开才能找到目标。我更建议默认收起、只展开第一级,既能展示层级,又不至于让页面过长。具体参数一般对应expandRow或某个初始化时是否自动展开的开关,不同插件命名有差异,原则是进入页面时只展开最高两层。
另一个经常被忽略的是点击区域。整行可点的实现方式,是在行绑定了点击事件后判断是否触发了折叠;如果插件只支持点箭头的,行绑定事件会覆盖掉插件的冒泡,导致点箭头没反应。排查时先确认当前插件点击的是整行还是仅箭头,再决定业务代码里要不要加冒泡阻止。这里有一个比较通用的配置思路:
// 展开折叠相关的一组推荐配置 $('#treeTable').bootstrapTable({ treeView: true, treeShowField: 'name', treeCollapseAll: false, // 初始化不全部收起 treeDefaultExpand: 1, // 默认展开层级:只展示两级 onExpandRow: function(index, row) { handleExpand(row); // 展开时的业务逻辑 }, onCollapseRow: function(index, row) { handleCollapse(row); // 收起时的业务逻辑 } });treeDefaultExpand的作用是控制初始展示深度,设为1表示展开根节点这一层,用户看到的是“华东大区”和“杭州分部”下的子级名,再往深的层级收起。onExpandRow和onCollapseRow是每次展开收起都会触发的回调,很多生产逻辑都要挂在这里——比如记录用户行为、异步加载子节点、联动其他图表。最容易被漏掉的一点是:这两个回调在表格重新加载时会反复触发,注意在回调里做去重和节流,避免重复请求接口。
4.2 异步加载子节点:按需拉数据而不是一次塞全量
树形表格真正走向生产环境的分水岭,是能否按需加载子节点。全量数据一次返回,结构再清晰也会在数据量上来后卡住。我经历过最夸张的场景是某次模拟项目X里一次返回了三万行,浏览器基本失去响应,用户点一下要等两三秒。后面改成异步加载后,首屏只渲染几百行,展开节点时才请求该节点的直接子级。
异步加载的机制大同小异:初始只加载根节点;展开节点时,插件用该行主键拼一个请求,把返回的数据挂到该节点下面;收起再展开,有的插件会重复请求,有的会缓存。重复请求是常见翻车点,所以我在接口参数里会带一个_t时间戳避免缓存,同时在业务层做简单缓存,展开过的节点短时间内不再重新请求。
onExpandRow: function(index, row) { if (row._loaded) return; // 已加载过的节点不再请求 $.get('/api/children', { parentId: row.id }) .done(function(res) { const children = res.data.map(item => ({ ...item, _loaded: false })); row._loaded = true; // 标记为已加载 $('#treeTable').bootstrapTable('appendData', children); }); }这里用了一个很简单的缓存标记_loaded,挂在节点对象上。请求成功并且子节点插入后,标记为已加载,下次再展开就不会触发重复请求。appendData是Bootstrap Table往表格追加数据的方法,在树形场景下它会自动把新数据挂在当前展开的节点下面。需要特别注意:如果子节点还有下级,新节点初始化时_loaded要置为false,否则它展开时也会被误判为“已加载过”,导致永远拉不到它的下级。
4.3 分页、排序、筛选在树形下的行为差异
普通表格和树形表格在分页、排序、筛选这三个功能上的行为完全不同,这是项目里最容易引发产品不满的重灾区。先说分页,普通表格分页只需要切页码,树形表格一旦分页,父节点和它的子节点会被拆到不同页,用户在前一页展开的节点切页后就没了,层级语义被彻底破坏。
常见做法是树形表格不做分页,或者只在顶级节点上做“虚拟分页”而子节点全部展开。如果你用的是Bootstrap Table的树形扩展,很多人会选择关闭分页,把分页改成“懒加载”——首屏只渲染部分根节点,滚动到接近底部时再追加下一批根节点。这样既保留了树形结构,又避免一次渲染过多。排序也是类似,只对当前已经渲染出来的行生效,不可能在异步子节点未加载时正确排到全量数据里。所以我的习惯是:树形场景下排序仅针对同一层级。
筛选需要特别注意。全局搜索普通表格是一个关键词匹配全部行,树形表格一旦搜索,父节点不匹配但子节点匹配时,父节点要不要显示?普遍预期是“显示父节点作为路径,但父节点本身不做高亮”。这是产品细节,需要在代码里做专门处理:匹配到子节点时,把它的所有祖先节点一并加入结果集,但只有真正命中的节点才做样式标记。
提示:如果你的需求里分页、筛选和树形展开一个都不能少,建议放弃“树形表格分页”这个念头,改为“树形结构 + 顶级节点补充筛选条件”。用一个筛选器先过滤出符合条件的顶级部门,再展开看下属成员,体验比硬分页好很多。
5. 树形表格踩坑实录:五个常见翻车点与排查路径
5.1 节点展开了但子行不显示
现象:点开箭头,父行下方空白,控制台没有报错,接口也返回了数据。当时排查了半天,最后发现是子节点的层级缩进占位列缺失。树形扩展渲染子行时,要为缩进单独预留一个单元格宽度,父级缩进列如果和其他列合并错了,子行渲染出来但宽度为0,视觉效果就像没显示。
原因:初始化时treeShowField指定的列宽度太小,或者该列在复杂表头里被动态合并列挤掉了。解决方式是把treeShowField这一列固定一个最小宽度,比如120px;如果项目用了冻结列插件,还要把树形缩进列设置为不冻结,冻结列会把展开箭头一起冻掉。
解决示例:给name列加width: 160,并且检查该列是否被设置为frozen: true,是的话改为false。
5.2 展开箭头不显示或点不动
现象:表格渲染出来了,层级数据也在,但行首没有箭头图标,或者箭头显示但点击无反应。这种问题大半出在图标字体库没加载。多数树形插件用字体图标绘制箭头,项目里如果为了优化体积把图标字体文件漏引了,箭头就显示成小方框或消失。
原因:图标库依赖缺失,或者插件的CSS被其他样式覆盖。解决:先打开开发者工具看箭头对应的元素是不是font-class,如果是,检查对应的字体文件HTTP请求是不是404。我曾经在某个项目里发现压缩后的CSS把::before的content值清空了,箭头全部消失,最后是调整CSS加载顺序解决的。
另外还有一个容易忽略的:如果你给行绑定了自己的点击事件做跳转,且没有阻止冒泡,点击箭头会先触发你的跳转,插件根本收不到这个点击。排查时先临时注释掉行点击事件,看箭头是否恢复点击。
5.3 分页后父节点被切走,展开状态丢失
现象:使用分页功能的树形表格,展开某父节点后翻到第二页再翻回来,之前展开的节点又合上了,用户觉得“刚看过的内容怎么消失了”。这个问题本质上是分页重新绘制了整个表格,树形状态没有持久化。
原因:Bootstrap Table的分页和树形扩展是两套独立机制,分页切页时表格重新渲染数据集合,树形扩展拿到的当前页数据里,没有保存“哪些节点被展开过”的记录。解决方式有两个方向:一是放弃分页,改成上面说的懒加载追加;二是把展开状态存到外部变量里,在onLoadSuccess或onPostBody回调里按id重新调用展开方法。
我实践下来更推荐第一种,因为保存展开状态再恢复,碰到异步子节点时会遇到恢复顺序问题——父节点还没展开,子节点数据也没有,状态恢复变成一件无比繁琐的事。如果产品强制要求分页,就把展开恢复做成“只恢复第一层”,再往深层让用户点击展开,不用全恢复。
5.4 异步子节点加载后展开状态失效
现象:第一次展开某节点,接口返回子节点并展开完毕;收起后再次展开,发现之前的新增内容消失,或者重新请求了一遍,甚至节点直接变成“无子节点”状态。
原因:大多数情况下是子节点被追加到了表格的根级而不是当前节点下。appendData的语义是把数据加到最后,如果树形扩展没能识别这些新数据的归属,它们会被渲染成一级节点。另一个常见原因是异步返回后没有重新调用treeView的刷新渲染方法。
解决方式:不要用零散的appendData,而是先从节点对象上拿到当前节点的children集合,push进子节点后调用一次“刷新树结构”的方法。具体方法名各插件不同,思路是一样的:先把数据挂到节点上,再告诉插件“这棵树的结构变过了,请重新计算缩进和箭头”。直接追加数据而不通知插件刷新,箭头和缩进状态就可能错乱。
5.5 大数据量渲染卡顿
现象:一次性渲染5000行以上,页面滚动明显掉帧,展开一个节点要卡半秒。很多人第一反应是“换个更快插件”,但我换过之后只能说改善有限,因为性能瓶颈主要不在插件,而在一次性生成太多DOM节点,以及每次展开时整表重绘。
原因:树形表格本质上还是大量<tr>节点,展开一个父节点后,插件默认会调用整表渲染逻辑把所有行重新画一遍,复杂度是O(n)。解决方向有两个:一是让展开只增量渲染子节点行,不重绘全局;二是做链路懒加载,每层最多渲染200行,更深的数据等用户展开时再请求。
我一般会做一层防御式控制:展开某节点前先判断子节点数量,超过500个就给出一个提示“该节点子项较多,首次展开可能较慢”,同时把展开动作改为只加载前100条。这样虽然用户体验上不是最优,但至少不会让整个页面崩溃。真正需要高效处理几万行层级数据时,建议考虑虚拟滚动方案,但那个就不属于“插件配置”层面了,需要换成基于虚拟列表的组件。
注意:排障时务必备份一份最小复现页面。把表格限定在一个固定高度容器里,只保留name一列,如果小页面里问题消失,那就是样式或列配置的锅,不是插件本身的锅。“在最小页面上复现问题”是我排查这五类问题最常用的手段,能快速和业务代码切割。
6. 进阶技巧:用懒加载和搜索过滤把树形表格真正投入生产
6.1 懒加载子节点的数据协议
生产环境里我会和后端约定一个统一的懒加载协议:请求参数固定传parentId,根节点传parentId=0;响应统一返回{ code: 0, data: [{ id, parentId, name, hasChildren }] }。其中hasChildren字段非常关键,它告诉前端这个节点下面还有没有子级,前端据此决定是否渲染展开箭头,避免出现“点开箭头发现没有子节点”的空状态。
比hasChildren更进一步,我还会让后端返回每个节点的预估子节点数量childCount,在前端文案里展示为“上海分部(3)”。这个细节在组织架构场景里很实用,用户一眼就知道展开后能看到多少内容,减少盲目点击。懒加载状态下,缩进深度对性能影响不大,但建议限制最大深度为5层,超过时提示“层级过深,请在数据库中调整”,避免异常数据把页面渲染卡死。
6.2 搜索过滤时保留父路径
全局搜索的最优体验是:搜索“浦东”,列表里直接展示“华东大区 > 上海分部 > 浦东组”这条路径,而不是把“华东大区”和“上海分部”单拎出来。实现时,我在buildTree阶段预先为每个节点生成_path字段,搜索时对_path做模糊匹配,命中的节点保留,同时把它的祖先节点一并标记为“路径节点”显示但不高亮。
function searchTree(nodes, keyword) { const result = []; nodes.forEach(node => { if (node.name.includes(keyword) || node._path.includes(keyword)) { result.push(node); pushAncestors(result, node._parentPath); // 补充祖先路径 } if (node.children && node.children.length) { const childMatches = searchTree(node.children, keyword); if (childMatches.length) { result.push({ ...node, children: childMatches, _isPathNode: true }); } } }); return result; }这段代码的精髓在于:子节点命中,父节点也进结果集,但父节点的children被替换成“过滤后的匹配结果”。这样父节点作为一个“路径容器”存在,用户看到的是完整一条路径,而不是孤零零一个匹配行。_isPathNode留给前端做样式区分,路径节点灰色显示,命中节点高亮。我用这种方式改造后,产品对搜索结果的可读性非常满意,不再抱怨“搜出来不知道在哪一层”。
6.3 导出树形数据时的顺序问题
树形表格导出是最后一块硬骨头。用户以为导出的是“所见即所得”,但很多导出插件是直读表格DOM,树形缩进在导出文件里变成一堆空格,层级完全丢失。常见做法是导出时改用扁平数据并附上_path列,每行显示完整的从根到当前的路径,用户拿到Excel后自己按路径拆分。这个方案实现简单,后端也不需要额外开发。
function flattenForExport(nodes, prefix) { const list = []; nodes.forEach(node => { list.push({ path: prefix + node.name, name: node.name }); if (node.children && node.children.length) { list.push(...flattenForExport(node.children, prefix + node.name + ' > ')); } }); return list; }导出的列就两列:一列显示“华东大区 > 上海分部 > 浦东组”,一列显示节点本身名称。这样用户在Excel里用按“>”分列就能还原层级,比“按缩进空格拆分”可靠得多。我吃过一次亏,之前的导出把缩进换成两个空格,结果用户拿到Excel里某个节点名自带空格,拆分结果直接错位,后来统一改成“>”分隔符,再也没出过问题。
最后说一个我养成的习惯:每次调整树形表格的初始化参数,我都会把改动固化成一份“树形表格参数自查表”,记录哪些参数在后端返回异常时可以临时屏蔽。因为树形插件的参数受数据格式影响很大,后端字段一改,前端就要跟着改。这个自查表帮我避免了好几次上线前才发现的回归问题。希望这些踩坑经验能帮你少走一段弯路,如果你也打算给Bootstrap项目加树形表格,先跑通最小Demo再谈参数优化,基本不会翻车。
本文还有配套的精品资源,点击获取