在Node.js服务端处理业务数据时,经常需要与Excel文件打交道,比如读取用户上传的清单、生成对账报表或者把数据库查询结果导出给运营人员。Node.js标准库并没有内置电子表格解析能力,因此通常要借助第三方模块。社区中常用的xlsx模块(SheetJS)就是其中之一,它使用纯JavaScript实现,同时支持浏览器和Node.js环境,能够读写xlsx、xls、csv等多种格式。该模块的API设计比较直接,只需要几行代码就能完成工作簿的加载、遍历和保存操作。下面按照安装、读取、写入和优化四个步骤来展开。

一、安装xlsx模块并理解工作簿模型
要使用xlsx模块,第一步需要通过npm将其安装到项目里。在项目根目录执行以下命令即可完成安装,安装成功后就可以在代码中引入使用。如果使用Yarn作为包管理器,也可以用yarn add xlsx来安装。模块本身没有原生依赖,所以在大多数服务器上安装过程都很顺利。
// 使用CommonJS方式引入
const XLSX = require('xlsx');
// 读取文件,返回工作簿对象
const workbook = XLSX.readFile('./data/example.xlsx');
console.log(workbook.SheetNames); // 输出所有工作表名称,例如 ['Sheet1', 'Sheet2']
在这段代码里,readFile方法负责加载本地Excel文件,并返回一个工作簿对象。这个对象包含两个核心属性:SheetNames是一个字符串数组,记录了文件中所有工作表的名称;Sheets是一个对象,键为工作表名称,值为对应的工作表数据。通过SheetNames拿到第一个工作表名称后,再用workbook.Sheets[sheetName]就能获取该工作表的详细信息。单元格数据在xlsx模块内部以对象形式存储,例如worksheet['A1']会返回类似{t: 's', v: '姓名', h: '姓名'}的结构,其中t表示单元格类型,v是实际值,h是格式化后的显示文本。
读取文件的路径可以使用相对路径或绝对路径。在Windows服务器上如果使用绝对路径,需要注意反斜杠转义问题,例如'C:\\data\\example.xlsx',或者直接改为正斜杠'C:/data/example.xlsx'。很多读取失败的问题都出在路径写错或者目录不存在,建议在调用readFile之前先检查文件是否存在,或者用try...catch捕获异常信息,避免服务因未处理的错误而中断。
二、读取Excel数据:从单元格到二维数组
直接操作单元格对象并不方便,实际开发中更常见的做法是把工作表转换成数组或JSON对象。xlsx模块提供了utils.sheet_to_json方法,它接受工作表对象作为参数,默认把第一行当作表头,将后续每一行转换为以表头为键的对象。如果第一行不是标题行,可以用header选项指定行号,传header:1则表示第一行也是数据,返回二维数组。
const XLSX = require('xlsx');
const workbook = XLSX.readFile('./data/students.xlsx');
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
// 转为对象数组,默认以第一行为键
const rows = XLSX.utils.sheet_to_json(worksheet);
console.log(rows);
// 输出类似 [{ 姓名: '张三', 年龄: 20, 班级: 'A班' }, ...]
// 转为二维数组,第一行也作为数据
const matrix = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
console.log(matrix);
// 输出类似 [['姓名','年龄','班级'], ['张三',20,'A班']]
对象数组形式很适合后续进行数据库写入或逻辑计算,而二维数组形式则便于保留原始行列结构,适合做数据校验或模板填充。两种转换方式都会跳过完全空白的行,但空单元格默认返回undefined,如果希望统一处理空值,可以在选项中加入defval参数,比如defval:null会把空单元格转换为null,避免后续代码因为undefined引发类型错误。
Excel中的日期和时间比较特殊,底层存储的是从1900年1月1日开始计算的天数序列值,直接读取会得到一个数字。如果需要在转换时自动得到JavaScript的Date对象,可以在readFile或read方法中设置cellDates:true。例如XLSX.readFile('./data/report.xlsx', { cellDates: true }),这样日期类型的单元格就会自动解析为Date对象,方便再进行格式化。但要注意,这个选项不能识别文本格式的日期,如果单元格本身就是字符串'2024/01/15',则仍然返回字符串,需要额外解析。
三、写入Excel文件:创建、填充与导出
生成Excel文件同样简单,核心思路是先创建空的工作簿,再把数据转换成工作表,最后写入本地文件。utils.book_new用于创建新工作簿,utils.json_to_sheet可以把JSON数组转换成工作表,utils.aoa_to_sheet则接收二维数组。生成的工作表通过book_append_sheet添加到工作簿中,再用writeFile方法保存为文件。
const XLSX = require('xlsx');
// 模拟数据库查询结果
const users = [
{ 姓名: '李雷', 工号: 'A001', 入职日期: '2024-03-10' },
{ 姓名: '韩梅梅', 工号: 'A002', 入职日期: '2024-04-22' }
];
// 创建新工作簿
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(users);
XLSX.utils.book_append_sheet(workbook, worksheet, '员工名单');
// 设置列宽,单位约为字符宽度
worksheet['!cols'] = [{ wch: 10 }, { wch: 12 }, { wch: 20 }];
// 写入本地文件
XLSX.writeFile(workbook, './output/users.xlsx');
这段代码生成了一个包含员工名单的Excel文件。json_to_sheet会自动把对象键作为表头,并按照对象键出现的顺序排列列。如果需要对列顺序进行调整,可以先把数据转成二维数组,再用aoa_to_sheet创建,这样表头和数据行都能完全控制。写入时如果需要保留前导零的编号,例如工号A001,要确保传入的是字符串,不能传数字,否则A001会变成1。若单元格本身就是数字但想强制按文本写入,可以构造单元格对象,例如{t: 's', v: '001'},再手动放置到工作表中。
列宽通过worksheet['!cols']设置,数组元素中的wch表示字符宽度。这个属性不影响数据本身,只是打开文件时的展示效果。如果需要设置行高或合并单元格,也可以操作worksheet['!merges']和worksheet['!rows'],但日常导出场景中列宽调整已经能满足大部分需求。
四、处理上传文件和性能优化
在Web服务中,读取Excel往往来自用户上传的文件,而不是服务器本地已有文件。此时文件内容通常以buffer形式存在于内存中,不适合先写临时文件再调用readFile。xlsx模块提供了read方法,可以直接解析buffer,用法是XLSX.read(buffer, { type: 'buffer', cellDates: true })。这种方式不仅减少磁盘IO,还能避免临时文件清理不及时造成的存储压力。
const XLSX = require('xlsx');
// 假设从HTTP上传接口拿到buffer,例如Multer的req.file.buffer
function parseUploadedExcel(buffer) {
const workbook = XLSX.read(buffer, { type: 'buffer', cellDates: true });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(firstSheet, { defval: null });
return rows;
}
defval选项在这里非常实用。上传的表格经常存在不连续的空单元格,如果不加处理,转换后的对象里对应字段会缺失或为undefined,后续计算容易报错。设置defval:null后空单元格统一变为null,与数据库中的NULL语义一致,也便于做数据清洗。读取上传文件时还要注意,xlsx模块不会执行Excel中的宏或公式,只读取公式计算后的缓存值,因此相对安全。但仍然不要信任文件的扩展名,应通过buffer内容解析,而不是只判断文件名后缀。
对于大文件,xlsx模块的read和readFile方法都会把整个工作簿加载到内存中,这在处理数十MB以上的文件时可能导致内存占用过高。可以通过read方法的options限制读取范围,例如设置sheetRows来限制每个工作表最多读取的行数,或者使用bookSheets和bookProps只加载指定工作表。如果业务上需要处理超大规模数据,建议在生成Excel时就按批次拆分文件,或者将xlsx模块用于中小文件的读写,超大文件则考虑使用专业ETL工具或流式解析库。
另一个常被忽略的点是错误处理。读取文件时可能遇到文件损坏、密码保护、格式不兼容等情况,这些异常会抛出错误。在使用readFile或read时,应当用try...catch包裹,并记录明确日志,方便定位是文件问题还是代码问题。同时,在写入文件时也要确认输出目录存在,否则writeFile会因为找不到目录而失败。可以在写入前用fs.existsSync检查目录,必要时递归创建目录。
从实际使用来看,xlsx模块对中小型Excel文件处理非常高效,API一体化程度高,读与写都能在少量代码内完成。无论是做定时报表导出,还是处理客户导入的名单数据,掌握上面这些写法就能覆盖大部分场景。配合buffer解析和defval空值处理,可以让服务端与Excel文件交互的代码更加健壮,也更容易维护。