jExcel 是前端里少有的“轻量但能打”的在线表格库。我最早接触它是做一个后台数据录入系统,需求是让运营直接在页面上维护一张报价单,要求可编辑、可增删行、能导出 Excel。调研了一圈,发现 jExcel 的 API 设计非常贴合这种场景——不需要引入 Angular、React,原生 JS 直接调,体积也小。这篇文章我就围绕“jExcel api 指引”这个主题,把项目里真正用到的 API、事件、以及和后端交互容易踩的坑整理成一份实战参考,适合正在做后台管理、报表配置、数据采集类前端的同学,尤其是那种“表格功能要给业务方用,但又不想上重型框架”的场景。
1. jExcel 项目概述与 API 体系设计思路
1.1 为什么要选 jExcel 而不是另一个表格库
先聊聊选型。当时我面前有几个选择:Handsontable、x-data-spreadsheet、以及 jExcel(现在叫 jspreadsheet CE)。Handsontable 功能确实强,但商业版要授权费,API 也偏重;x-data-spreadsheet 界面漂亮,但更偏向“类 Excel 工具”,跟后端数据交互时反而不够直接。
jExcel 最戳我的点是:它是一个 JavaScript 原生的电子表格库,核心文件不大,不依赖框架,上手成本极低。你用jexcel(el, options)就能把任意 div 变成一个可编辑表格,然后通过实例方法对表格进行增删改查,API 命名也很直觉,getValue、setValue、insertRow、deleteRow,基本看一眼就知道是干嘛的。
我做了一个简单的对比表,方便大家根据自己的项目选型:
| 对比维度 | jExcel / jspreadsheet CE | Handsontable | x-data-spreadsheet |
|---|---|---|---|
| 体积 | 轻量,核心几十KB | 较大,功能模块多 | 中等 |
| 框架依赖 | 无,原生 JS 可用 | 框架无关但集成较复杂 | 框架无关 |
| API 直观程度 | 高,方法命名直白 | 中,需要查文档 | 中 |
| 离线可编辑 | 支持 | 支持 | 支持 |
| 导出 Excel/CSV | 内置下载 API | 插件支持 | 支持 |
| 商用授权 | 宽松(CE 版) | 商业版收费 | MIT |
如果你的场景是“给业务做一个快速可编辑的数据表格,并且要能轻松把数据回传到后端”,jExcel 是非常顺手的选项。它不会强加给你一整套数据结构,反而更接近“我用一个二维数组渲染表格,也用一个二维数组取回数据”的直觉。
1.2 jExcel API 体系的整体脉络
新手最容易懵的,是 jExcel 的 API 到底有几类。我把它拆成三块,理解这三块之后,后面所有问题都会清晰很多。
第一块是全局初始化函数jexcel()。它有两种主要调用方式:一种是jexcel(el, options)用来创建表格;另一种是jexcel(el),后面只传容器元素,用来获取已经存在的表格实例。第二种方式在事件回调里非常常用,因为你可以在任何地方通过这个 API 拿到当前表格,然后调用它的方法。
第二块是表格实例方法。比如getData()、setData()、getValue()、setValue()、insertRow()、deleteRow()、mergeCells()等等。这些方法是操作表格的主入口,掌握了它们就掌握了表格的动态交互能力。
第三块是事件回调,比如onchange、onload、oninsertrow、ondeleterow。它们不是主动调用的 API,而是 jExcel 在特定时机反向通知你的钩子。比如用户改了某个单元格,onchange就会触发,你能在这个时机去处理联动逻辑或向后端提交变更。
这里分享一个我很早踩过的坑:jExcel 4.x 之后官方把项目改名为 jspreadsheet CE,但很多老教程至今还在用 jExcel 的名字。你下载最新的 jspreadsheet CE 之后,引入路径是jspreadsheet.umd.js,而老项目里写的jquery.jexcel.min.js依然能跑,但版本是旧的。API 大体兼容,但个别方法在新版里有参数调整,所以做项目时一定要锁定一个版本,别混着用。
2. 初始化配置项:掌握表格的基础 API 参数
2.1 常用初始化配置项拆解
创建表格,本质上是往jexcel(el, options)里塞配置对象。大家可以理解成:el是“画布”,options是“图纸”。图纸上定义了表格分成几列、列宽多少、第一行显示什么、允不允许排序之类。
下面这个例子是我项目里的初始配置,可以直接拿去改:
const el = document.getElementById('myTable'); const table = jexcel(el, { data: [ ['张三', 18, '男', '北京'], ['李四', 22, '女', '上海'], ['王五', 25, '男', '广州'] ], columns: [ { title: '姓名', width: 120 }, { title: '年龄', width: 80, type: 'numeric', mask: '#' }, { title: '性别', width: 100, type: 'dropdown', source: ['男', '女'] }, { title: '城市', width: 120 } ], colHeaders: ['姓名', '年龄', '性别', '城市'], rowHeaders: true, tableOverflow: true, columnSorting: true, minDimensions: [4, 5], onchange: (instance, cell, col, row, value, oldValue) => { console.log(`第${row}行第${col}列由 ${oldValue} 改为 ${value}`); } });这段配置里几个关键项我再强调一下。
data是一个二维数组,它的顺序和表格行列一一对应。columns是列定义数组,里面每一项对应一列,可以设置title、width、type。colHeaders控制表头文字,rowHeaders控制左侧行号是否显示。tableOverflow是个容易被忽略的好东西,表格数据多时,它可以让表格在容器内部滚动,而不是把页面撑爆。columnSorting允许点击表头排序,业务方很爱这个功能。minDimensions指定最小行列数,即使没有数据,表格也会渲染出一个默认大小的空白区域,方便用户直接点格子录入。
提示:
columns里的title是列标题,但它不直接等于表头文字。真正控制表头显示的是colHeaders。如果你只设置了columns.title,有些版本会显示 title,有些版本则要求必须设置colHeaders才能稳定显示表头。项目里我建议两个都写上,既保证显示稳定,也方便别人看代码时快速知道列含义。
2.2 列类型与数据源格式选择
jExcel 支持多种列类型,最常用的有text、numeric、hidden、dropdown、checkbox、calendar、color。不同列类型决定单元格的可视化形态和编辑组件。
dropdown跨列关联是我项目里经常用的一个功能。比如“城市”这一列,用户不需要手填,而是从下拉列表里选。配置方式就是在columns里给对应列设置type: 'dropdown',并用source提供选项数组:
{ type: 'dropdown', source: ['北京', '上海', '广州', '深圳'], width: 120 }checkbox类型适合做状态标记。比如“是否启用”列,你只要配置type: 'checkbox',单元格就能直接勾选。取值时,勾选状态会变成true或false,非常方便。
还有一个比较实用的类型是calendar,它能把单元格变成日期选择器,配置后用户点格子就能弹日历,返回的日期格式可以通过options控制:
{ type: 'calendar', options: { format: 'YYYY-MM-DD' }, width: 140 }数据源格式上,后端接口返回的通常是对象数组,比如:
const apiData = [ { name: '赵六', score: 90, pass: true }, { name: '钱七', score: 70, pass: false } ];而 jExcel 原生接受的是二维数组,所以要先做一次转换:
const tableData = apiData.map(item => [item.name, item.score, item.pass]); jexcel(el, { data: tableData });为什么要这么设计?因为 jExcel 的定位就是“轻量的 Excel 体验”,Excel 本身就是以行列坐标为基准,而不是以字段名。把它理解成一张二维表,表头是字段名,数据行是记录,后面读写操作都基于坐标,思路就顺了。
3. 实例方法实战:动态操纵表格的 API
3.1 单元格级 API:取值、赋值、合并
实例方法是我们日常写交互逻辑时接触最多的部分。先说单元格级操作。
getValue(cellName)和setValue(cellName, value)是最常用的两个方法。这里的cellName支持两种写法:一种是“A1”这样的坐标字符串,另一种是getValue(row, col)的双索引形式。我习惯在业务代码里用坐标字符串,因为在 JS 里它更直观,尤其是在循环里拼接时:
const instance = jexcel(el); // 获取 A1 单元格 const name = instance.getValue('A1'); // 给 A2 单元格赋新值 instance.setValue('A2', '刘八');setValue还有一个重载用法,可以一次给多个单元格赋值,减少重复调用:
instance.setValue([ ['A2', '刘八'], ['B2', '30'], ['C2', '男'] ]);合并单元格是另一个常见需求。比如做一个年度汇总表,标题行要合并到整行宽度。这时用mergeCells():
instance.mergeCells('A1:D1');合并之后,表格会把这四个单元格当成一个整体显示。如果你想知道当前哪些区域被合并了,可以执行:
const merges = instance.getMerge(); console.log(merges);这个方法返回一个数组,里面是合并区域的坐标信息,适合做复杂报表时做归档判断。
注意:
setValue会触发onchange事件。如果你在onchange里又写了一段setValue,要小心死循环。我一般在联动计算时加一个逻辑开关,比如记录一个isUpdating布尔值,在程序赋值时跳过后续联动逻辑。
3.2 行列级 API:插入、删除、移动、隐藏
表格的动态增删行是业务里的高频操作。用户点“新增一行”,前端调insertRow();用户删掉一行,前端调deleteRow(索引)。
const instance = jexcel(el); // 在末尾追加一行 instance.insertRow(); // 在索引为 1 的位置插入一行 instance.insertRow(1); // 删除索引为 2 的行 instance.deleteRow(2); // 在末尾追加一列(默认用第1列的类型) instance.insertColumn(); // 在索引为 0 的位置插入一列 instance.insertColumn(0);insertRow和insertColumn都支持第二个参数,比如插入行时可以带上初始数据:
// 在第 2 行位置插入一行,并给这行预填三个单元格的值 instance.insertRow(2, ['默认姓名', 0, '男']);还有两个很容易被忽略的方法:moveRow(from, to)和moveColumn(from, to)。它们用于拖拽排序的业务,比如用户要把“北京”这条记录上移一行,就可以这样:
// 把第 3 行移动到第 1 行 instance.moveRow(3, 1);行列移动后数据会自动重排,同时行号列也会跟着变,不需要手动刷新,这个体验比我自己写 DOM 操作时要省心太多。
隐藏列和隐藏行的方法也值得一提。hideColumn(index)、hideRow(index)可以按索引把指定列或行藏起来,但数据还在表格里,适合做“表格里存了 ID,但不想让用户看到 ID 列”这种需求。显示时调用showColumn(index)、showRow(index)即可。比如我在列表里留了一个主键 ID 列,配置到hidden类型的列里,用户看不到但取值时能拿到,非常实用。
3.3 数据读取与回写:getData、setData、download
表格数据的读取和回写,是整个 API 体系中后端同学最关心的。最常用的就是getData():
const instance = jexcel(el); const rows = instance.getData(); console.log(rows);返回结果是一个二维数组,每一行是数组里的一个元素,单元格值按列顺序排列。如果你需要把表头一起带上,可以用getData(true),这样返回的数据会多一行表头信息,便于后端生成 Excel 或 CSV。
要整表替换数据,用setData():
const newRows = [ ['赵六', 26, '女', '深圳'], ['孙九', 30, '男', '杭州'] ]; instance.setData(newRows);整表替换和逐格赋值最大的区别在于:setData是一次性重建,性能更好,适合“切换查询条件后重新加载表格”这类场景;逐格setValue适合局部更新和联动计算,比如修改某个单元格之后只刷新这个格子的状态。
还有一个很实用的 API 是download()。它可以直接把表格导出为 CSV 或 XLSX:
// 导出为 CSV,文件名是 report.csv instance.download('csv'); // 导出为 xlsx instance.download('xlsx');在项目交付时,这类导出功能往往是领导必点、用户必用的功能,用内置 API 就省掉了我自己拼文件格式的麻烦。
4. 事件回调与联动:让表格“活”起来
4.1 核心事件回调解析
事件回调是 jExcel API 里最有价值的部分,也是表格从“静态展示”变成“业务交互”的关键。
最常用的是onchange。它的回调参数包含instance、cell、col、row、value、oldValue。我在项目里用它做数据变更联动、实时统计、甚至自动保存。
这个事件除了value是新值、oldValue是旧值之外,还有一个细节:col和row都是从 0 开始的索引。比如你表头是“姓名、年龄、性别”,用户改了“年龄”这一列,那么col就是 1,而不是 2,更不是 Excel 里的 B 列序号,写代码时别弄混。
onload是初始化完成后触发的回调。如果你需要一开始就根据后端数据做一些渲染(比如把某个单元格标红、合并某些区域),在onload里操作是最稳妥的,因为此刻表格已经完整渲染出来了。
还有oninsertrow、ondeleterow、oninsertcolumn、ondeletecolumn这些事件。我通常用ondeleterow来做二次确认:用户删除一行时,先记录下被删数据,再调后端接口删除,如果后端失败就insertRow把数据塞回来。
4.2 联动案例:下拉联动与自动计算
联动是表格 API 比较出效果的一个场景。我做一个“城市 - 学校”联动下拉时,就是利用onchange拿到城市列的新值,动态修改学校列的下拉选项。
假设表格有“城市”列和“学校”列。城市改变时,学校列要更新成对应城市的学校列表:
const cityMap = { '北京': ['海淀大学', '朝阳学院'], '上海': ['浦东大学', '徐汇学院'], '广州': ['天河大学', '越秀学院'] }; const instance = jexcel(el, { data: [['北京', '海淀大学']], columns: [ { title: '城市', width: 120, type: 'dropdown', source: ['北京', '上海', '广州'] }, { title: '学校', width: 160, type: 'dropdown', source: ['海淀大学', '朝阳学院'] } ], onchange: (inst, cell, col, row, value, oldValue) => { // 城市列 col 为 1 if (col === 1) { const schools = cityMap[value] || []; inst.setColumnType(2, 'dropdown', { source: schools }); inst.setValue(row, 3, ''); } } });这里有几个细节值得注意:setColumnType可以动态改变列的编辑类型和配置;先清空旧值再更新下拉来源,避免用户看到旧学校名称残留;如果城市没有对应的学校列表,我把下拉源设为空数组,用户就无法选中错误的值。
自动计算也是一个典型场景。比如表格里有“数量”“单价”“金额”三列,用户改了数量或单价,金额要自动算出结果。我在onchange里取当前行的这两个值,计算结果后用setValue回写金额列,并且加了之前提到的isUpdating开关,防止setValue再次触发onchange造成反复计算:
let isUpdating = false; onchange: (inst, cell, col, row, value, oldValue) => { if (isUpdating) return; if (col === 1 || col === 2) { const qty = Number(inst.getValue(row, 2)); const price = Number(inst.getValue(row, 3)); if (!isNaN(qty) && !isNaN(price)) { isUpdating = true; inst.setValue(row, 4, qty * price); isUpdating = false; } } }这种“事件 + 实例方法”的组合写法,基本能覆盖大部分表格业务逻辑。记住一个原则:事件回调负责“听”,实例方法负责“做”,两者配合而不是混在一起,代码才不容易乱。
5. 常见问题与排错实录
5.1 初始化与渲染问题
表格初始化不显示,是我见过最多的问题。排查时先看容器宽度。jExcel 跟很多表格库一样,依赖容器尺寸来渲染,如果容器的width为 0 或者根本没宽度,表格就会“消失”。解决办法是给容器一个固定宽度或最小宽度:
#myTable { width: 100%; min-width: 600px; }还有一种是初始化时机不对。如果容器是动态生成的,或者在你执行jexcel()时还没插入 DOM,也会导致渲染失败。正确做法是先确保容器在文档里,再初始化。如果是在 Vue、React 的mounted/useEffect里操作,要等 DOM 挂载完成后再调用,不要在数据请求回调里立刻初始化,更保险的做法是先setTimeout或requestAnimationFrame等一帧。
我把这类初始化问题整理成一个速查表:
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 表格空白不显示 | 容器没有宽度或父级隐藏 | 给容器设置宽度,检查父级 display |
| 初始化后只看到一个小方块 | 容器尺寸过小 | 设置 min-width、min-height |
| 表格错位、列宽异常 | 初始化时容器宽高未稳定 | 延迟初始化或在 resize 后重新渲染 |
| 中文乱码 | 页面编码不是 UTF-8 | 设置<meta charset="UTF-8"> |
5.2 表格数据提交后端接口返回 400 的排查套路
很多同学在实际项目里会遇到“表格数据提交后端接口返回 HTTP 400”的情况。比如后端接口要求{ "name": "张三", "age": 18 },但你拿着getData()直接fetch上去,后端就报 400。
我自己的排查流程是分三步走。
第一步,先把getData()的结果打印到控制台,看是不是二维数组,以及数组里每个元素的类型是否符合预期。很多时候,原因是数字在表格里被编辑成了字符串,后端要 number 却收到 string,自然校验不过。
第二步,跟后端确认接口的出入参格式。如果后端要求的是对象数组而不是二维数组,就需要在提交前做一次转换:
const rows = instance.getData(); const payload = rows.map(row => ({ name: row[0], age: Number(row[1]), city: row[2] })); fetch('/api/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });第三步,如果接口还是返回 400,就用 Postman 或 ApiPost 直接请求同一个接口,用同样的 JSON 数据。如果请求依然失败,说明问题不在前端,而在接口本身或参数校验规则。这一步能快速切分前后端责任范围。
还有一种特别容易出现的 400 是“字段不一致”:表格里某一列是隐藏的 ID,你没设置hidden类型,导致用户编辑后 ID 列被当成普通数据传给了后端,后端在解析时发现 schema 对不上,直接拒绝。解决办法是把不参与业务校验的列设成type: 'hidden'或者提交前从数组中剔除对应索引。
5.3 版本升级与框架集成注意事项
jExcel 在 4.x 之后改名为 jspreadsheet CE,严格来说这两个名字对应的不是完全相同的产物,但很多人混着叫。老项目如果用jquery.jexcel.min.js且依赖了 jQuery,新项目我建议直接用jspreadsheet.umd.js,纯原生即可,不用再为兼容老语法做额外工作。
在 Vue 和 React 项目里集成时,我强烈建议封装一层组件,把实例挂到组件的内部变量,然后用onChange等事件往父组件抛数据,而不是让外部直接操作 DOM。这样可以避免组件卸载后表格实例还在,造成内存泄漏或者事件回调重复绑定。
销毁表格时用destroy()方法:
const instance = jexcel(el); // 组件卸载时调用 instance.destroy();还有一个从老版本升级容易踩的坑是:getValue的参数顺序在新版里更严格了。老版本允许不传参数批量获取,新版则更推荐用getData()搞定全量读取。所以升级时要全局搜索一遍getValue的调用点,逐个确认参数是否符合当前版本要求。
不要盲目升级到最新版。表格这种组件跟业务耦合度高,只要当前版本稳定、能满足需求,就锁死小版本,做好回归测试再考虑升。我用 jExcel 时专门在 package.json 里锁了版本号,避免自动升级带来的行为差异。
再分享一个小技巧:当表格数据量较大时,频繁setValue会造成明显卡顿。我的做法是先把要修改的数据整理成数组,然后一次性用循环调用,并在操作前后用instance.updateSettings或直接重新setData代替多次局部写入。实测下来,几十行数据没感觉,几百行以上时这个优化效果比较明显。
踩过几次坑之后,我的体会是:jExcel 的 API 并不难,难的是搞清楚它“以坐标为中心”的设计思路。只要理解了表格本质上是二维数组、事件是交互入口、实例方法是对数组的 CRUD,你就能把它灵活用到各种后台报表场景里。最后再提醒一句,正式项目里不要直接在业务代码里满屏散落jexcel(el)调用,封装成统一的数据表格组件,把初始化配置、事件绑定、数据回写都收敛在一个地方,后续维护会轻松很多。