前端导出10万条数据/前端导出Excel/WebWorker优化导出Excel

lxf2023-04-20 20:48:02

导读

  1. 要导出10万条数据?后端处理好数据、加工成excel后,前端再导出!
  2. 啥?前端自己加工、自己导出?!开玩笑,10万数据,啥终端都给你卡爆!
  3. 后端也卡?你后端不会开启多线程啊!叫我大佬啊,好吧!其实前端也可以开启多线程的~
  4. 用webworker,咋用?在哪儿获取数据、加工数据、生成excel?卡不卡?线程间如何通讯?卡不卡?

背景

ToB端项目,需要查看大量数据,在前台展示又很影响性能,我考虑过用indexDB存储数据,然后前端做虚拟列表,来展示数据,或者分页,也就是涉及“前端渲染10条数据”这种面试题,想必在坐的各位再熟悉不过了。但领导觉得还是这么查看不方便,那就导出为excel啊!

导出excel,原本是让后端处理好,前端调接口,然后导出的,就很快。但出于种种原因,现在要完全靠前端处理,想都不用想,前端处理绝对卡爆!但非得前端处理(说我是大佬,肯定有办法!光荣啊,谢谢您嘞!这咋整?考验我有没有两把刷子?不是有无刷子问题,办法总比困难多,是费事费时费头发的问题!能不能给我留点,我还没找对象!)。

也就是,现在的需求是:前端在无后端协助下,导出excel表

接下来我们来看看前端导出会有哪些棘手的问题。

一、10万条数据直接导出

这么处理,光10万条数据(10W个对象) 不做任何处理 直接导出 ,想都不用想,根据经验,页面这都得卡10S。

我们来测试一下:

前端导出10万条数据/前端导出Excel/WebWorker优化导出Excel

这只是10W条最简单的数据,如下图这种:

前端导出10万条数据/前端导出Excel/WebWorker优化导出Excel

都没加工处理,CPU爆满,耗时8S 。

这8.236S 你页面的任何交互 直接挂了

前端导出10万条数据/前端导出Excel/WebWorker优化导出Excel

页面的渲染也会卡顿。

我们把这个数据量提升到20W

20W的时候 直接达到了32s !!!

这都还没对数据做处理

直接循环模拟的数据 我都没敢上接口

前端导出10万条数据/前端导出Excel/WebWorker优化导出Excel

注意:

形成新的数组,用concat还是push好?

我的直觉是push好

实测的确是

二、加工成表格导出(需要后端配合)

前端页面导出excel文件功能(导出)

后端返回给前端的 blob 数据,前端转换表格导出(见scdn链接:纯前端导出表格)

xxxApi(params).then(res=>{
	if(res){
          const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
          const a = document.createElement('a')
          a.download = '表格.xlsx'
          a.href = window.URL.createObjectURL(blob)
          a.click()
          console.log('导出成功')
	}else{
		console.log('导出失败')
	}
})

这是后端加工成表格,我们其实只是下载

那在前端加工成表格,并导出呢?

三、加工成表格导出(不需要后端配合)

1、通过 js-xlsx

vue2\vue3\react,中的使用,见scdn链接:纯前端导出表格

2、通过 vue-json-excel

见scdn链接:纯前端导出表格

这两种方法,数据量小可以,但我们是10万条数据,这种方法太卡了

纯前端加工成表格太卡,又不能后端参与,只能前端,那前端要是能新开个线程就好,恰好前端新方法WebWorker,可以开启子线程

四、使用WebWorker

使用中会出现这几个问题:

1、在主线程还是子线程加工成表格好?

1.1在主线程加工成表格

10万数据8秒多,20万32秒。这还没对数据加工,实际数据是需要处理的。比如修改日期格式、给收益率这种加上百分号、再比如吧某些正负数值转换成盈/亏。数据量大 ,还放在主线程循环,恭喜你,可以收获一个几十秒,啥也干不了的页面。

1.2那在子线程中加工成表格

在子线程中使用三-1,三-2的方法,会出现新的问题,他们的源码里面用到了dom,而子线程里获取不到dom。

2、在主线程还是子线程导出好?

如果在子线程处理数据 ,在主线程导出。那就会出现,子线程传递主线程的开销(序列化json开销大), 大于你直接放在主线程处理的开销,负优化了,属于是。

因为主线程与 worker 线程之间的通信是拷贝关系,当需要传递一个巨大的二进制文件给 worker 线程处理时(worker 线程就是用来干这个的),这时候使用拷贝的方式来传递数据,无疑会造成性能问题。

3、WebWorker介绍

  1. WebWorker 允许在主线程之外再创建一个 worker 线程,在主线程执行任务的同时,worker 线程也可以在后台执行它自己的任务,互不干扰。
  2. 主线程:调用new Worker()构造函数,新建一个 worker 线程,构造函数的参数是一个 url,生成这个 url 的方法有两种:1脚本文件(会有两个限制);2字符串形式(需要new Blob([data]) 将数据转成二进制,)。
  3. 子线程:self.onmessage监听主线程传过来的信息,self.postMessage发送信息给主线程,self.close()worker 线程关闭自身。Worker 线程能够访问一个全局函数 imprtScripts() 来引入脚本,该函数接受 0 个或者多个 URL作为参数。
  4. 因为 worker 创造了另外一个线程,不在主线程上,浏览器给设定了一些限制(无法使用:window 对象、document 对象、DOM 对象、parent 对象;可以使用:浏览器:navigator 对象、URL:location 对象 只读、发送请求:XMLHttpRequest 对象、定时器:setTimeout/setInterva、应用缓存:Application Cache)
  5. 因为主线程与 worker 线程之间的通信是拷贝关系,当需要传递一个巨大的二进制文件给 worker 线程处理时(worker 线程就是用来干这个的),这时候使用拷贝的方式来传递数据,无疑会造成性能问题。Web Worker 提供了一种转移数据的方式,允许主线程把二进制数据直接转移给子线程。这种方式比原先拷贝的方式,有巨大的性能提升。只是要注意一旦数据转移到其他线程,原先线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面
// 创建二进制数据
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
    uInt8Array[i] = i;
}
console.log(uInt8Array.length); // 传递前长度:33554432
// 字符串形式创建worker线程
var myTask = `
    onmessage = function (e) {
        var data = e.data;
        console.log('worker:', data);
    };
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));

// 使用这个格式(a,[a]) 来转移二进制数据
myWorker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]); // 发送数据、转移数据

console.log(uInt8Array.length); // 传递后长度:0,原先线程内没有这个数据了
  1. 当要导出的数据量极大的时候,主线程与 Worker 线程之间还使用对象形式进行数据通信就会造成性能问题,所幸 WebWorker 在线程之间的通信支持以二进制形式数据进行通信。项目当中使用的为new Blob([string])来对导出的数据进行二进制形式转换,恰巧 file-saver 也可以使用 Blob 二进制数据形式,进行对文件的导出
FileSaver.saveAs(new Blob([outputString], {
  type: 'application/octet-stream'
}), `${fileName}.xlsx`)

4、最终解决步骤

所以我们如何处理?

一句话

在子线程中,获取源数据、处理要导出的数据,处理完传递给主线程,主线程接收后保存为excel文件

每步详解

  1. 在子线程(webwork)中,获取源数据(worker 接收 init 初始化信号并且利用 XMLHttpRequest 发送请求获取数据),(疑问:为什么不在主线程接收好数据,然手用上述第5点的方式,将数据以二进制方式传递给子线程,子线程再接收?而要在子线程发请求获取数据?)
  2. 子线程处理组装要导出的数据(加载 excel.js 并进行数据转换,使用介绍:使用exceljs导出excel表格,ExcelJS 使用帮助文档),
  3. 子线程把数据传递给主线程(这里是使用二进制数据形式在 worker 线程与主线程之间进行传输上述经过 excel.js 转换后的数据),
self.postMessage({
  type: 'success',
  data: {
    // 转二进制
    xlsxBlob: new Blob([wbout], {
      type: 'application/octet-stream'
    }),
  }
});
  1. 主线程接收 Worker 线程数据,保存为excel文件(Worker 线程已经将数据转换成二进制了,所以,调用 file-saver saveAs 方法后,数据直接传入,就可以保存为 Excel 文件)
import FileSaver from 'file-saver'

worker.onmessage = (event) => {
  const msgType = event.data.type

  switch (msgType) {
    case 'success':
      FileSaver.saveAs(event.data.data.xlsxBlob, `${fileName}.xlsx`)
      resolve()
      break

      // ...
     
    default: break
  }
}

详细见文章:前端性能优化之 WebWorker 优化数据导出下载 Excel 操作