使用nodejs/exceljs读取、操作、写入excel文件
'# 使用nodejs/exceljs读取、操作、写入excel文件
一、背景与问题
在企业级应用开发中,Excel文件处理是一个常见需求。传统开发中,处理Excel文件通常需要依赖第三方库,而Node.js生态中,exceljs库提供了强大的功能支持。但开发者在使用过程中常常面临以下问题:
- 如何高效处理大文件(如数百万行数据)
- 如何处理复杂格式(如样式、公式、图表)
- 如何保证数据一致性与安全性
- 如何在不同版本之间保持兼容性
- 如何处理各种异常情况(如文件损坏、格式错误)
本文将深入探讨exceljs的工作原理、实现细节、最佳实践和常见陷阱,帮助开发者在实际项目中合理使用这一工具。
二、基本原理
exceljs的核心原理基于XLSX.js库,其工作流程分为三个阶段:
- 文件解析:将Excel文件(.xlsx/.xls)的二进制数据解析为内存中的数据结构
- 数据操作:通过DOM-like API对工作表、行、单元格进行增删改查
- 文件生成:将内存中的数据结构转换为Excel文件格式
其底层使用了zip.js处理zip压缩包,使用XML解析器处理工作表数据,通过CSS选择器语法进行单元格定位(如worksheet.getRow(1).getCell('A'))。
三、环境准备
npm install exceljs推荐版本:exceljs@4.3.0(最新稳定版)
需要同时安装的依赖:
npm install xlsx注意:不同版本的exceljs对xlsx库的依赖版本有差异,需注意兼容性。
四、核心实现
1. 基础读取操作
const ExcelJS = require('exceljs');
const fs = require('fs');
async function readExcel(filePath) {
const workbook = new ExcelJS.Workbook();
try {
await workbook.xlsx.readFile(filePath);
const worksheet = workbook.getWorksheet('Sheet1');
// 读取所有行
const rows = worksheet.getRow(1, worksheet.rowCount);
console.log('读取到行数:', rows.length);
// 读取特定单元格
const cell = worksheet.getCell('A1');
console.log('单元格内容:', cell.value);
// 读取格式信息
console.log('单元格样式:', cell.style);
} catch (error) {
console.error('读取错误:', error.message);
}
}关键点解释:
- 使用
xlsx.readFile方法异步读取文件 getWorksheet方法获取工作表,支持按名称或索引获取getRow方法支持分页读取,避免一次性加载全部数据getCell方法可获取单元格的值和样式信息
2. 高级写入操作
const ExcelJS = require('exceljs');
const fs = require('fs');
async function writeExcel(filePath) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
// 设置单元格样式
worksheet.getCell('A1').value = '测试数据';
worksheet.getCell('A1').style.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {argb: 'FF00FF00'}
};
// 添加行数据
worksheet.addRow(['ID', '名称', '数量']);
worksheet.addRow([1, '商品A', 100]);
worksheet.addRow([2, '商品B', 200]);
// 写入文件
await workbook.xlsx.writeFile(filePath);
}关键点解释:
- 使用
addWorksheet创建新工作表 - 支持设置单元格的多种样式(字体、边框、填充等)
addRow方法支持批量添加行数据writeFile方法支持异步写入文件
3. 数据操作示例
const ExcelJS = require('exceljs');
async function manipulateExcel() {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
// 添加数据
worksheet.addRow(['Name', 'Age', 'Country']);
worksheet.addRow(['Alice', 25, 'USA']);
worksheet.addRow(['Bob', 30, 'China']);
// 操作数据
const row = worksheet.getRow(2);
row.getCell('B').value = 35; // 修改年龄
row.getCell('C').value = 'Japan'; // 修改国家
// 添加公式
worksheet.getCell('D1').value = '=SUM(B2:B3)';
// 保存文件
await workbook.xlsx.writeFile('output.xlsx');
}关键点解释:
- 支持直接修改单元格的值
- 可以添加公式(支持常见运算符)
- 支持复杂的数据操作(如合并单元格、设置边框等)
五、完整案例:用户数据导出系统
1. 前端上传接口(Express)
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), async (req, res) => {
try {
const file = req.file;
if (!file) {
return res.status(400).send('No file uploaded');
}
const excelFile = await processExcel(file.path);
res.download(excelFile, 'processed_data.xlsx');
} catch (error) {
res.status(500).send(error.message);
}
});2. 后端处理逻辑(核心部分)
const ExcelJS = require('exceljs');
const fs = require('fs');
const path = require('path');
async function processExcel(filePath) {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(filePath);
const worksheet = workbook.getWorksheet('Sheet1');
// 清洗数据
const rows = worksheet.getRow(1, worksheet.rowCount);
const cleanedData = rows.map(row => {
const rowData = {};
for (let i = 1; i <= row.values.length; i++) {
rowData[`${String.fromCharCode(64 + i)}`] = row.getCell(i).value;
}
return rowData;
});
// 创建新文件
const newWorkbook = new ExcelJS.Workbook();
const newWorksheet = newWorkbook.addWorksheet('Processed Data');
// 写入数据
newWorksheet.addRow(['ID', 'Name', 'Country']);
cleanedData.forEach(data => {
newWorksheet.addRow([data.ID, data.Name, data.Country]);
});
// 保存文件
const newFilePath = path.join(__dirname, 'processed_data.xlsx');
await newWorkbook.xlsx.writeFile(newFilePath);
return newFilePath;
}3. 安全处理
function validateFile(file) {
const allowedExtensions = ['.xls', '.xlsx'];
const ext = path.extname(file.originalname).toLowerCase();
if (!allowedExtensions.includes(ext)) {
throw new Error(`Unsupported file type: ${ext}`);
}
// 检查文件大小
if (file.size > 10 * 1024 * 1024) { // 10MB
throw new Error('File size exceeds limit');
}
}六、源码解析
exceljs的源码结构主要包括:
- Workbook类:处理整个工作簿的创建、读写、工作表管理
- Worksheet类:处理单个工作表的行、列、单元格操作
- Cell类:处理单元格的值、样式、公式等
- Reader/Writer类:处理文件的读写逻辑
关键代码片段(简化版):
class Workbook {
constructor() {
this.worksheets = [];
}
addWorksheet(name) {
const worksheet = new Worksheet(name);
this.worksheets.push(worksheet);
return worksheet;
}
async readFile(filePath) {
const data = await fs.readFileSync(filePath);
// 解析zip文件
const zip = new JSZip(data);
const workbook = await zip.loadAsync();
// 解析工作表数据
this.parseWorkSheets(workbook);
}
parseWorkSheets(workbook) {
// 解析XML数据,创建worksheet对象
}
}七、进阶使用
1. 处理大文件
async function processLargeFile(filePath) {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(filePath);
// 分页读取
const pageSize = 1000;
const rowCount = workbook.worksheets[0].rowCount;
for (let i = 1; i <= rowCount; i += pageSize) {
const endRow = Math.min(i + pageSize - 1, rowCount);
const rows = workbook.worksheets[0].getRows(i, endRow);
// 处理数据...
}
}2. 处理复杂格式
const worksheet = workbook.addWorksheet('Sheet1');
worksheet.getColumn(1).width = 30; // 设置列宽
worksheet.getColumn(2).numFmt = '0.00'; // 设置数字格式
worksheet.getRow(1).height = 20; // 设置行高3. 处理公式和图表
worksheet.getCell('D1').value = '=SUM(B2:B3)';
worksheet.getCell('D1').style.font.bold = true;
// 添加图表
const chart = workbook.addChart({
type: 'bar',
title: { text: 'Sales Data' },
legend: { show: true },
series: [
{ name: 'Sales', data: [10, 20, 30] }
]
});八、性能与工程实践
1. 性能优化策略
| 优化策略 | 说明 |
|---|---|
| 分页处理 | 避免一次性加载全部数据 |
| 流式处理 | 使用readFile的流式接口 |
| 压缩数据 | 使用zip压缩减少传输体积 |
| 缓存数据 | 对常用数据进行缓存 |
2. 内存管理
处理大文件时需注意内存占用,可使用以下策略:
// 使用流式读取
workbook.xlsx.readFile(filePath, {
type: 'buffer',
callback: (err, buffer) => {
// 处理buffer数据
}
});3. 安全风险控制
- 文件类型验证:严格校验文件扩展名和MIME类型
- 内容过滤:防止恶意代码注入(如公式攻击)
- 权限控制:限制用户对文件的访问权限
- 沙箱环境:对用户上传文件进行隔离处理
九、常见问题与踩坑
1. 常见错误及解决方法
| 错误类型 | 错误示例 | 解决方案 | ||
|---|---|---|---|---|
| 文件读取失败 | Error: ENOENT: no such file or directory | 检查文件路径和权限 | ||
| 单元格值丢失 | cell.value === null | 使用`cell.value | '默认值'`处理 | |
| 格式转换错误 | TypeError: Cannot read property 'value' of undefined | 添加空值判断 | ||
| 内存溢出 | Error: Out of memory | 使用分页处理或流式处理 |
2. 常见陷阱
- 格式兼容性问题:不同版本的Excel文件格式差异
- 样式丢失:未正确设置单元格样式
- 公式计算错误:未正确设置公式依赖关系
- 图表不显示:未正确配置图表数据源
十、最佳实践
1. 推荐使用场景
- 需要处理大量数据(10万+行)时
- 需要保持格式完整性的场景
- 需要复杂样式和公式处理的场景
- 需要跨平台兼容性的场景
2. 不推荐使用场景
- 需要处理CSV文件时(推荐使用csv-parser)
- 需要处理JSON格式数据时(推荐使用jsonfile)
- 需要处理小文件时(推荐使用fs模块)
3. 推荐方案
- 使用流式处理处理大文件
- 使用分页读取避免内存溢出
- 使用严格校验机制防止安全风险
- 使用缓存机制提升性能
十一、总结
exceljs作为Node.js处理Excel文件的强大工具,具有以下特点:
- 支持多种文件格式(.xlsx/.xls)
- 提供丰富的API进行数据操作
- 支持复杂格式(样式、公式、图表)
- 具备良好的扩展性
在实际开发中,需要注意:
- 合理选择处理方式(分页/流式/缓存)
- 严格校验文件类型和内容
- 注意内存管理
- 处理异常情况
通过合理使用exceljs,可以有效提升数据处理效率,但需注意其适用场景和限制。在处理复杂数据时,建议结合其他工具(如csv-parser、jsonfile)形成完整的解决方案。
评论已关闭