前言
在项目开发过程中,导出 Excel 文件是很常见的功能。根据生成 Excel 文件的方式,通常分为后端生成和前端生成。
前端实现下载后端生成的 Excel 通常有两种方式,①是后端返回 Excel 文件的临时下载地址前端直接下载,②是后端返回 ArrayBuffer 二进制数据,前端处理后下载。
而前端生成 Excel 就简单的多了,后端仅需返回指定格式的 JSON 数据就可以了,生成 Excel 这一步由前端浏览器完成,这样就可以大大减轻服务器压力,节约服务器资源。但是前端在主线程内对大量数据进行excel导出时不可避免的会对主线程进行阻塞,造成页面卡顿,影响用户体验;
Web Worker ?
可以先查看另一篇文章 Web Worker API
SheetJS
SheetJS,又叫 XLSXJS,官网称为 SheetJS。它支持浏览器、nodejs、deno、和 react-native,浏览器兼容 ie10+。
SheetJS 社区版提供经过实战考验的开源解决方案,用于从几乎所有复杂的电子表格中提取有用的数据,并生成新的电子表格,这些新的电子表格可以与传统软件和现代软件一起使用。
下面是一个导出 十万行 20列 数据的一个示例:
const cloLen = 20;
const rowLen = 100000;
const row = Array(cloLen).fill().reduce((t, e, i) => ({ ...t, [`字段${i + 1}`]: `字段${i + 1}的值` }), {});
const list = Array(rowLen).fill({ ...row });
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(list, { dense: true });
XLSX.utils.book_append_sheet(wb, ws);
XLSX.writeFile(wb, `${new Date().toLocaleTimeString()}.xlsx`, {
bookSST: true,
});
因为 JavaScript 的单线程特性,大量的 JS 运算会导致浏览器渲染进程阻塞,出现不同程度的浏览器假死现象。
以我的 Mac Pro为例,生成一个十万行 20列的 Excel 约执行 1700 ~ 1800ms,同时浏览器的也会卡死这么长时间。随着行列数量的增加,这个时间会随着线性增加。
这个时候就需要使用 Web Worker来避免浏览器渲染阻塞了。
const cloLen = 20;
const rowLen = 100000;
const row = Array(cloLen).fill().reduce((t, e, i) => ({ ...t, [`字段${i + 1}`]: `字段${i + 1}的值` }), {});
const list = Array(rowLen).fill({ ...row });
const SheetJSWebWorker = new Worker(
URL.createObjectURL(
new Blob([
`
importScripts("https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js");
onmessage = ({ data }) => {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(data, { dense: true });
XLSX.utils.book_append_sheet(wb, ws);
postMessage(XLSX.write(wb, { type: "array", bookType: "xlsx", bookSST: true, compression: true }))
};
`,
])
)
);
SheetJSWebWorker.postMessage(list);
SheetJSWebWorker.onmessage = ({ data }) => {
const a = document.createElement("a");
a.download = `${new Date().toLocaleTimeString()}.xlsx`;
a.href = URL.createObjectURL(
new Blob([data], { type: "application/octet-stream" })
);
a.click();
};
此时生成 Excel 的执行时间大约为 2400 ~ 2500ms。但是页面渲染丝毫没有任何阻塞。
需要注意的是 SheetJS 是一个商业项目。我们使用的是完全免费的社区版。社区版仅提供了生成 Excel、合并单元格等很有限的功能。我们想要自定义生成 Excel 的样式,需要借助一些 SheetJS 的开源周边,比如:xlsx-style
和 xlsx-populate
等。这里就不展开了,有兴趣大家自行研究。
如果想要导出有自定义功能的 Excel,就需要借助 ExcelJS 实现了。
ExcelJS
ExcelJS 提供了更为强大的自定义导出 Excel 功能,但是性能只有 SheetJS 的四分之一。
单线程示例:
const cloLen = 20;
const rowLen = 100000;
const row = Array(cloLen).fill().reduce((t, e, i) => ({ ...t, [`字段${i + 1}`]: `字段${i + 1}的值` }), {});
const list = Array(rowLen).fill({ ...row });
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet();
worksheet.columns = Object.keys(list[0]).map((e) => ({ header: e, key: e, width: 20 }));
worksheet.addRows(list);
const a = document.createElement("a");
a.download = `${new Date().toLocaleTimeString()}.xlsx`;
a.href = window.URL.createObjectURL(
new Blob([await workbook.xlsx.writeBuffer()], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8",
})
);
a.click();
多线程示例:
const cloLen = 20;
const rowLen = 100000;
const row = Array(cloLen).fill().reduce((t, e, i) => ({ ...t, [`字段${i + 1}`]: `字段${i + 1}的值` }), {});
const list = Array(rowLen).fill({ ...row });
const ExcelJSWebWorker = new Worker(
URL.createObjectURL(
new Blob([
`
importScripts("https://cdn.bootcdn.net/ajax/libs/exceljs/4.3.0/exceljs.js");
onmessage = async ({ data }) => {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet();
worksheet.columns = Object.keys(data[0]).map(e => ({ header: e, key: e, width: 20 }));
worksheet.addRows(data);
postMessage(await workbook.xlsx.writeBuffer())
};
`,
])
)
);
ExcelJSWebWorker.postMessage(list);
ExcelJSWebWorker.onmessage = ({ data }) => {
const a = document.createElement("a");
a.download = `${new Date().toLocaleTimeString()}.xlsx`;
a.href = window.URL.createObjectURL(
new Blob([data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8",
})
);
a.click();
};
CSV
大多数时候,我们导出的 Excel 文件也可以用 CSV 格式生成。
从名字上看一个是导出 CSV,一个是导出 Excel。
那么这二者有什么区别呢?
-
Excel 是一个电子表格,将文件保存为自己的专有格式,即 xls 或 xlsx,它保存有关工作簿中所有工作表的信息。
-
CSV 代表 Comma Separated Values ,这是一个纯文本格式,用逗号分隔一系列值,但不包含格式,公式,宏等。
总结来说**,Excel 不仅可以存储数据,还可以存放对数据的操作结果,CSV 文件只是一个文本文件,它只存储数据,因此十分容易生成**。
同时 CSV 文档使用 word 打开后默认也是以 Excel 的形式展现,只需要简单的处理就可以变为 Excel。
单线程示例:
const cloLen = 20;
const rowLen = 100000;
const row = Array(cloLen).fill().reduce((t, e, i) => ({ ...t, [`字段${i + 1}`]: `字段${i + 1}的值` }), {});
const list = Array(rowLen).fill({ ...row });
let str = Object.keys(list[0]).join() + "\n";
for (let i = 0; i < list.length; i++) {
for (const key in list[i]) {
str += `${list[i][key] + "\t"},`;
}
str += "\n";
}
const a = document.createElement("a");
a.href = "data:text/csv;charset=utf-8,\ufeff" + encodeURIComponent(str);
a.download = `${new Date().toLocaleTimeString()}.csv`;
a.click();
多线程示例:
const cloLen = 20;
const rowLen = 100000;
const row = Array(cloLen).fill().reduce((t, e, i) => ({ ...t, [`字段${i + 1}`]: `字段${i + 1}的值` }), {});
const list = Array(rowLen).fill({ ...row });
const VanillaJSWebWorker = new Worker(
URL.createObjectURL(
new Blob([
`
importScripts("https://cdn.bootcdn.net/ajax/libs/exceljs/4.3.0/exceljs.js");
onmessage = async ({ data }) => {
let str = Object.keys(data[0]).join() + String.fromCharCode(10)
for (let i = 0; i < data.length; i++) {
for (const key in data[i]) {
str += data[i][key] + '\t,';
}
str += String.fromCharCode(10);
}
postMessage('data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(str))
};
`,
])
)
);
VanillaJSWebWorker.postMessage(list);
VanillaJSWebWorker.onmessage = ({ data }) => {
const a = document.createElement("a");
a.download = `${new Date().toLocaleTimeString()}.csv`;
a.href = data;
a.click();
};
总结
前端生成 Excel 的最大优点就是可以减少服务器资源的消耗,充分利用了客户端的算力资源,将计算压力由服务器转移到浏览器,其次就是传输数据量更小,相对而言更快。
同时缺点也很明显,首先 JavaScript 并不属于高计算性能的编程语言,相对计算性能比不上服务器计算。其次,生成导出文件的快慢最重要的影响因素取决于用户的硬件,不用性能的电脑可能存在较大的用户体验差异。
下面是以上几个方案的常规适用场景,请参考:
- 百万级数据使用 CSV。
- 大量数据无样式功能要求使用 SheetJS。
- 少量数据功能齐全的使用 ExcelJS。
- 大量数据需要功能使用 商业版 SheetJS。
相关文档链接
使用Web Worker优化代码
Web Workers | MDN
Web Worker 使用教程 - 阮一峰的网络日志
WebWorker 优化数据导出下载 Excel 用户操作体验