指南 · 专家
版本基线 ExcelJS 4.x。深入工程实践:流式读写大文件、样式引用与克隆陷阱、工作表保护、列宽自适应、解析提速、以及与 SheetJS 的选型边界。
一、流式写入大文件(WorkbookWriter)
普通 Workbook 把整个文档保留在内存,几十万行会 OOM。流式写入用 stream.xlsx.WorkbookWriter,配 commit() 边写边释放:
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
filename: 'large.xlsx',
useStyles: true, // 让流式输出也带样式
useSharedStrings: true, // 重复字符串去重,减小文件体积
});
const ws = workbook.addWorksheet('数据');
ws.columns = [
{ header: 'ID', key: 'id', width: 10 },
{ header: '值', key: 'value', width: 15 },
];
for (let i = 1; i <= 500000; i++) {
ws.addRow({ id: i, value: Math.random() }).commit(); // 每行 commit
}
await ws.commit(); // 提交工作表
await workbook.commit(); // 完成文件commit 的代价
一旦某行被 commit,它会被写入输出流并从内存丢弃,不再可访问——这正是流式不爆内存的原因,代价是无法回头修改已提交的行。所以流式写入要「数据备齐再逐行写」。
useSharedStrings: true 让重复文本走共享字符串表(只存一份、用索引引用),重复文本多时显著减小体积;useStyles: true 让流式写入也输出样式(默认关闭以省开销)。
二、流式读取大文件(WorkbookReader)
const workbook = new ExcelJS.stream.xlsx.WorkbookReader({ filename: 'large.xlsx' });
for await (const worksheet of workbook) {
for await (const row of worksheet) {
// 边读边处理,不把整个文件载入内存
console.log(row.values);
}
}也可监听事件:workbook.on('worksheet', ws => ws.on('row', row => {...}))。
三、样式引用与克隆陷阱
ExcelJS 的样式对象(font/fill/border 等)按引用共享。把同一个对象赋给多格后再改它,会牵连所有引用它的单元格:
const base = { name: 'Arial', size: 12 };
ws.getCell('A1').font = base;
ws.getCell('A2').font = base;
base.size = 20; // ❌ A1、A2 同时变成 20 号!需要独立时克隆后再赋值:
const make = (over) => ({ name: 'Arial', size: 12, ...over });
ws.getCell('A1').font = make({ bold: true }); // 各自独立
ws.getCell('A2').font = make({ italic: true });反过来,若你故意想「一改全改」,复用同一引用就是特性。关键是分清「何时复用、何时克隆」。
四、工作表保护与单元格锁定
单元格 protection.locked 只有在工作表被保护后才生效:
// 默认所有单元格 locked=true;先把可编辑区放开
ws.getCell('B2').protection = { locked: false };
// 再保护工作表(口令可选)
await ws.protect('your-password', {
selectLockedCells: true,
selectUnlockedCells: true,
});只设 locked 而不调用 ws.protect(...),编辑不受任何限制。protection 还有 hidden(隐藏公式)。
五、列宽自适应(无内置 autoFit)
ExcelJS 没有内置列宽自适应,需手动按最长文本估算:
ws.columns.forEach((column) => {
let max = 10;
column.eachCell({ includeEmpty: true }, (cell) => {
const len = (cell.value ?? '').toString().length;
if (len > max) max = len;
});
column.width = max + 2; // 留点余量
});中文字符视觉宽度约为西文两倍,精细场景可对中文字符按 1.6~2 倍加权。
六、解析提速:ignoreNodes
读取超大、含大量数据校验/超链接的文件时,可让解析跳过不关心的 XML 节点:
await workbook.xlsx.load(buffer, {
ignoreNodes: ['dataValidations', 'hyperlinks'],
});只关心数据、不需要这些元素时,能明显降低解析开销与内存。
七、getWorksheet(id) ≠ worksheets[0]
官方特意提醒这条易错点:
workbook.getWorksheet(1); // 按工作表 id(删表后 id 可能不连续!)
workbook.worksheets[0]; // 按数组下标,永远是第一张
workbook.getWorksheet('名字'); // 按名取,最稳妥删除过某张表后,id=1 可能取不到你以为的首表——按位置取请用数组下标,按业务取请用名字。
八、ExcelJS vs SheetJS 选型
| 维度 | ExcelJS | SheetJS(xlsx) |
|---|---|---|
| 强项 | 带样式/格式的写出(报表导出) | 广泛格式的解析与数据转换 |
| 样式写入 | 精细(font/fill/border/条件格式/校验/图片) | 社区版有限 |
| 支持格式 | xlsx / csv | xlsx/xls/csv/ods 等众多 |
| 流式 | stream.xlsx 读写省内存 | 有流式 API |
| 公式计算 | 不求值(需缓存 result) | 不求值 |
| 典型场景 | 生成漂亮的带样式报表 | 解析各种来源的杂格式 / 纯数据进出 |
一句话:要好看的样式导出选 ExcelJS,要解析多种来源格式或纯数据进出选 SheetJS。两者是独立实现,无封装关系,必要时也可组合使用。