基于worker实现excel报表导出下载
业务背景
在前端对透视表数据导出为excel
进行下载,透视表数据最高可到百万个单元格以上,因此在主线程内对大量数据进行excel
导出时不可避免的会对主线程进行阻塞,而主线程阻塞对于用户感知来说就是页面卡顿;
显然,这种业务是一种典型的CPU
密集型任务,如果可以放在主线程以外的线程进行执行那么就可以有效的避免对主线程的阻塞了;Worker 正是为此而生,通过创建worker
来新建线程,就可以有效的分担主线程的压力;
关于 Worker
至于worker
的使用,可以参考这个——使用 Web Workers - Web API 接口参考 | MDN,其实还挺简单的;更不用说像vite
这种dev server
还内置了worker-loader
,开箱即用(顺便说一句,vite
也对WASM
提供了开箱即用的加载,心动😁)。
worker
虽好,但是其运行上下文跟主线程还是不一样的,实际上是一个子集语法,因此DOM
对象和很大一部分的BOM
对象都不能访问和使用;具体限制可以参考——Web Workers 可以使用的函数和类 - Web API 接口参考 | MDN。
Worker 的通信
主线程和Worker
实例之间都是通过postMessage
方法来向对方发送消息,而又通过onmessage
方法来注册各自的消息接收处理事件。
如何在 Worker 中实现 excel 的导出
前端有个著名的excel
导出库——sheetjs
,幸运的是这个库的大部分方法都能在worker
中直接进行使用,因此基本上可以参照正常的导出逻辑搬到worker
文件中即可。这里不会探讨sheetjs
的具体使用细节(因为这种API
使用文档网上太多了,就不赘述了),假设此时可以获取到对透视表表格本身的单元格二维数组(即以行为主导,每行里面的元素就是列),那么根据二维数组就能生成一个WorkBook
对象。
而有了WorkBook
对象,正常来说基于XLSX.writeFile()
等方法就能直接进行excel
下载操作了;一般现在的文件下载操作都是通过a
标签来实现的,而worker
本身是限制DOM
访问的,因此不能通过上述方法在worker
内实现下载操作,所以应该将下载操作放在主线程,而将必要的数据传送到主线程。
那么问题来了,应该选择传送什么样的数据到主线程?
思路一:传送 WorkBook 对象
从直觉上来说直接把WorkBook
对象传送到主线程,然后让主线程继续接着XLSX.writeFile()
方法的路子不就好了。但事实证明,这种思路有两个弊端:
- 线程间的通信成本:
WorkBook
对象本质上是一个包含大量数据的对象结构(包含单元格数据和单元格设置等),而线程间的通信成本会随着数据量加大而陡增(结构化克隆,序列化,反序列化等等);当数据量较大时,主线程在接收来自Worker
的消息依然会造成明显的阻塞。 - 从
WorkBook
对象到下载的成本:由于WorkBook
对象本身不能直接进行下载,因此XLSX.writeFile()
这类方法内部都需要先将WorkBook
对象转为Blob/File
对象,然后通过URL.createObjectURL()
来创建一个可访问的Blob URL
;但实际上将WorkBook
对象转为Blob/File
对象也是一个挺耗时的过程。
这里可以看一个具体导出过程的用时统计即可看出上述弊端:
综上,如果采用该方案,那么worker
的使用效果就很尴尬😅了。
思路二:传送 Blob URL
幸运的是,worker
内可以使用URL.createObjectURL()
方法[1],且创建的Blob URL
可以被主线程访问[2]!
到此,剩下的问题就只有一个了——那就是如何将WorkBook
对象转为Blob
对象;事实上,sheetjs
本身就提供了将WorkBook
对象转为二进制数据(Uint8Array
)的API
——XLSX.write(),所以这事就很简单了。
至此,只需要将Blob URL
传送到主线程即可完成最后的下载步骤;显然,传送一个URL
字符串的通信成本几乎为0,这样就可以把excel
导出下载任务的绝大多数运算控制在worker
内部了,最小可能的对主线程进行占用。
如何将透视表数据转换为单元格二维数组
这里渲染透视表采用的是S2这个库,这个库其实提供了一个将当前表格数据导出为csv
字符串的API
[3](基于此字符串可以分割得到单元格二维数组);而这个方法运行也是很耗时的,因此需要转移到worker
内部进行执行。不过不幸的是这个实例方法本身并不能直接移植到worker
(因为其依赖违反了限制),因此需要自行在worker
实现一个将透视表数据转为单元格二维数组的方法。
这里已知的信息有:
- 行维度信息
- 列维度信息
- 指标维度信息
- 按照行、列、指标维度拼接的对象数组(即数组元素里面是一个对象,该对象的
key
为行、列、指标维度,value
就是该维度的值)
根据上述信息可以构建出一个行头树结构和一个列头树结构:
然后根据指标分布在行或列的不同,在行头或列头树结构中的叶结点再添加相应的指标结点作为真实的叶结点,最终构成一个完整的行头结构。而根据行头路径(路径指的是自顶向下到一个叶结点所经过的结点)、列头路径再加上对应的指标key
就可以确定一个单元格的位置,所以基于此就可以构建出具体指标数据对应的单元格唯一key
;既然都知道每个单元格的数据分布在哪了,那么只需要根据行头和列头叶结点做**两层遍历(二维)**即可将所有数据单元格进行填充了。
相关文档
- 纯前端生成Excel文件骚操作——WebAssembly & web workers - SegmentFault 思否
- SheetJs + xlsx-style 导出附带样式的excel – cqh
- gitbrent/xlsx-js-style: SheetJS Community Edition + Basic Cell Styles:可以设置一些单元格样式
- 数字转换成26进制字母EXCEL列号(js实现)_const菜鸡的博客-CSDN博客
- 使用js-xlsx插件导出多级表头excel | liyang’s blog
- https://juejin.cn/post/6989437152341786655
- SheetJS(js-xlsx、XLSX)横向纵向合并单元格 - 知乎
- 前端 sheetjs 导出 excel 设置百分比格式
- SheetJS js-xlsx文档 | Ame’s blog
- https://stackoverflow.com/questions/280389/how-do-you-find-out-the-caller-function-in-javascript:使用`console.trace()`方法可以很好的打印出当前调用栈,比`caller`好用
- javascript - SheetJs Convert workbook back to arraybuffer - Stack Overflow
- javascript - How to save .xlsx data to file as a blob - Stack Overflow
- js有办法可以catch stack overflow(cpu 100%占用)吗?