前端可以不用依靠后端实现导出大数据了

美食家大橙子  论坛元老 | 2025-2-13 15:38:33 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1010|帖子 1010|积分 3030


theme: channing-cyan
hightlight: channing-cyan


媒介

在我们公司表格数据导出都是前端去处理。一开始数据量不大,倒没什么问题。但随着数据量的加大,问题也逐渐袒露出来。
一天的数据量有一来万条,导出一定时间范围的数据,30天就得30来万条数据。
那会测试直接给我导出 60 万条数据都存到一个 Excel 表中,页面直接卡死掉,动都动不了,后面直接崩溃掉。
那会为什么导出选择由前端去做呢?


  • 多语言问题:有些内置数据(如:文件分类,盘算机组等信息)需要支持多语言,以及表格 header 头。
  • 数据转换问题:有些内置数据返回的是数值类型,需要转成对应的真正的数据。
  • 导出表格字段问题:用户可以通过切换列来控制具体导出哪些字段。
排除原因

颠末排查:导出大量数据通常涉及大量的盘算、DOM 操作或文件生成等复杂操作,这些操作会在主线程中执行。假如这些操作耗时过长,主线程会被壅闭,导致页面无法响应用户交互(如点击、滚动等),表现为页面卡死
那是否把这些大量的盘算、DOM 操作或文件生成等复杂操作,放到子进进程去处理,不就办理了吗?
这就说到了今天的主角:Web Workers
Web Workers 先容

Web Workers 使得一个Web应用程序可以在与主线程分离的后台线程中运行一个脚本。
这样做的利益在于可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被壅闭。
   它的作用就是给JS创造多线程运行环境,允许主线程创建worker线程,分配任务给后者,主线程运行的同时worker线程也在运行,相互不干扰,在worker线程运行竣事后把结果返回给主线程。这样做的利益是主线程可以把盘算密集型或高延迟的任务交给worker线程执行,这样主线程就会变得轻松,不会被壅闭或拖慢。这并不意味着JS语言本身支持了多线程能力,而是浏览器作为宿主环境提供了JS一个多线程运行的环境。
  不过因为worker一旦新建,就会不停运行,不会被主线程的活动打断,这样有利于随时响应主线程的通性,但是也会造成资源的浪费,以是不应过度使用,用完注意关闭。或者说:假如worker无实例引用,该worker空闲后立刻会被关闭;假如worker实列引用不为0,该worker空闲也不会被关闭。
  Web Workers 使用


  • 创建 Worker 对象:通过 new Worker(url) 创建一个 Worker 对象,这里的 url 指向你预先编写的 JavaScript 文件路径,这个文件内包罗 Workers 将要执行的脚本内容。
  • 发送消息:你可以使用 worker.postMessage(message) 方法从主脚本向 Worker 发送数据。
  • 处理 Worker 发送的消息:在主脚本中,设置 worker.onmessage 事件监听器来处理 Worker 发回来的数据。
  • 停止 Worker:假如不再需要 Worker,可以调用 worker.terminate() 方法来制止 Worker。
  • 监听错误:可以通过添加 onerror 事件监听器来处理 Worker 中大概出现的错误。
主线程脚本
  1.   const myWorker = new Worker('worker.js')
  2.   const nums = [10, 20]
  3.   myWorker.postMessage(nums)
  4.   myWorker.onmessage = function(e) {
  5.     result = e.data
  6.     console.log('主进程接收子进程传递回来的数据:', e.data)
  7.     // 停止 Worker
  8.     worker.terminate()
  9.   }
  10.   myWorker.onerror = function(e) {
  11.     console.log('监听错误')
  12.   }
复制代码
Worker 脚本
  1. onmessage = function(e) {
  2.   var data = e.data;
  3.   var result = data[0] * data[1];
  4.   postMessage(result);
  5. }
复制代码
Web Workers 实战 Excel 导出

根本案例有了,但照旧遇到一些坑。下面开始一个个填坑。
问题1:vue 项目如何配置 web worker

这里需要下载第三方 loader, 来编译 workers 脚本。
npm install worker-loader@3.0.8
接下来,修改 vue.config.js 文件:
  1. // vue.config.js
  2. module.exports = {
  3.   chainWebpack(config) {
  4.     config.module
  5.       .rule('worker')
  6.       .test(/\.worker\.js$/)
  7.       .use('worker-loader')
  8.       .loader('worker-loader')
  9.       .options({})
  10.       .end()  
  11.   }
  12. }
复制代码
注意:test() 设置了文件名后缀是 .worker.js 则为 worker 脚本文件。
到这里第一个问题就办理了。。。

问题2:修改了 web worker 后,重新编译打包没有见效

vue项目一改动到代码文件就会重新编译。
但在调试过程中,修改了 worker 脚本,发现不停没有修复到问题,一开始也是很怀疑自己是不是逻辑堕落了。
通过 debug 才发现,代码不停没有修改。
后面每次修改 worker 脚本,都会重新启动 vue 项目,一开始问题是办理了。
但偶尔照旧会没有修改到代码。
最终排查到:原来是每次重新编译时,要删撤除 node_modules 目录下的 .cache 文件夹

才会重新加载新 worker 脚本代码
问题3:主进程向子进程发送参数时,若参数存在对象,会报错

这里紧张是生产 csvData 数据(key: value)中的 value 是一个对象结构时,发送给到 子进程,浏览器会报错。
这里办理方法是:将 value 举行序列化处理
  1. // * 判断 csvData 中的值是否存在对象,需要序列化处理
  2. const keys = csvHeader.map(item => item.key)
  3. csvData = csvData.map(row => {
  4.   return keys.reduce((acc, prev) => {
  5.     acc[prev] = typeof row[prev] === 'object' ? JSON.stringify(row[prev]) : row[prev]
  6.     return acc
  7.   }, {})
  8. })
复制代码
问题4:在子进程中下载文件失败

主进程去结合实际业务逻辑生成 csvHeader、csvData 数据后,发送给到子进程,由其生成 Excel 文件流,并下载下来。
  1. // 主进程
  2. const { csvHeader, csvData } = generateExcelData(data)
  3. // 子进程
  4. import Excel from 'exceljs'
  5. self.onmessage = async function(e) {
  6.   const { csvData, csvHeader } = e.data
  7.   const workbook = new Excel.Workbook()
  8.   const worksheet = workbook.addWorksheet('My Sheet')
  9.   worksheet.columns = csvHeader
  10.   csvData.forEach(row => worksheet.addRow(row))
  11.   // 生成 Excel 文件的 Buffer
  12.   const excelBuffer = await workbook.xlsx.writeBuffer()
  13.   // TODO 下载文件
  14. }
复制代码
颠末调试发现文件下载不下来,查阅资料得出:
紧张原因在于 Web Workers 的计划限制。具体来说,Web Workers 没有直接访问浏览器的 DOM 和一些与用户界面交互的功能,包括文件下载
以是这里只能将 Excel 文件的 Buffer转成blog发送给到主进程举行文件下载。
主进程
  1. import { saveAs } from 'file-saver'
  2. import ExportWorker from './export.worker.js'
  3. const worker = new ExportWorker()
  4. worker.postMessage({
  5.   csvData: csvData,
  6.   csvHeader: csvHeader
  7. })
  8. worker.onmessage = async(e) => {
  9.   const { chunk: blog } = e.data
  10.   saveAs(blog, filename)
  11. }
复制代码
worker 脚本
  1. import Excel from 'exceljs'
  2. self.onmessage = async function(e) {
  3.   const { csvData, csvHeader } = e.data
  4.   const workbook = new Excel.Workbook()
  5.   const worksheet = workbook.addWorksheet('My Sheet')
  6.   worksheet.columns = csvHeader
  7.   csvData.forEach(row => worksheet.addRow(row))
  8.   // 生成 Excel 文件的 Buffer
  9.   const excelBuffer = await workbook.xlsx.writeBuffer()
  10.   const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
  11.   self.postMessage({ chunk: blob })
  12. }
复制代码
源码

主进程
  1. import { saveAs } from 'file-saver'
  2. import ExportWorker from './export.worker.js'
  3. /**
  4. * 导出数据为 XLSX(通过 web Worker)
  5. * @param {Object} csvHeader XLSX 头
  6. * @param {Array} csvData 数据
  7. * @param {String} filename 文件名
  8. */
  9. const exportDataToXLSXByWorker = (csvHeader, csvData, filename) => {
  10.     const worker = new ExportWorker()
  11.     // * 判断 csvData 中的值是否存在对象,需要序列化处理
  12.     const keys = csvHeader.map(item => item.key)
  13.     csvData = csvData.map(row => {
  14.       return keys.reduce((acc, prev) => {
  15.         acc[prev] = typeof row[prev] === 'object' ? JSON.stringify(row[prev]) : row[prev]
  16.         return acc
  17.       }, {})
  18.     })
  19.     worker.postMessage({
  20.       csvData: csvData,
  21.       csvHeader: csvHeader
  22.     })
  23.     worker.onmessage = async(e) => {
  24.       const { chunk: blog } = e.data
  25.       saveAs(blog, filename)
  26.     }
  27. }
复制代码
worker 脚本
  1. import Excel from 'exceljs'
  2. self.onmessage = async function(e) {
  3.   const { csvData, csvHeader } = e.data
  4.   const workbook = new Excel.Workbook()
  5.   const worksheet = workbook.addWorksheet('My Sheet')
  6.   worksheet.columns = csvHeader
  7.   csvData.forEach(row => worksheet.addRow(row))
  8.   // 生成 Excel 文件的 Buffer
  9.   const excelBuffer = await workbook.xlsx.writeBuffer()
  10.   const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
  11.   self.postMessage({ chunk: blob })
  12. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

美食家大橙子

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表