当前位置: 首页 > article >正文

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


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 使用

  1. 创建 Worker 对象:通过 new Worker(url) 创建一个 Worker 对象,这里的 url 指向你预先编写的 JavaScript 文件路径,这个文件内包含 Workers 将要执行的脚本内容。
  2. 发送消息:你可以使用 worker.postMessage(message) 方法从主脚本向 Worker 发送数据。
  3. 处理 Worker 发送的消息:在主脚本中,设置 worker.onmessage 事件监听器来处理 Worker 发回来的数据。
  4. 终止 Worker:如果不再需要 Worker,可以调用 worker.terminate() 方法来停止 Worker。
  5. 监听错误:可以通过添加 onerror 事件监听器来处理 Worker 中可能出现的错误。

主线程脚本

  const myWorker = new Worker('worker.js')
  const nums = [10, 20]

  myWorker.postMessage(nums)

  myWorker.onmessage = function(e) {
    result = e.data
    console.log('主进程接收子进程传递回来的数据:', e.data)
    // 停止 Worker
    worker.terminate()
  }

  myWorker.onerror = function(e) {
    console.log('监听错误')
  }

Worker 脚本

onmessage = function(e) {
  var data = e.data;
  var result = data[0] * data[1];
  postMessage(result);
}

Web Workers 实战 Excel 导出

基本案例有了,但还是遇到一些坑。下面开始一个个填坑。

问题1:vue 项目如何配置 web worker

这里需要下载第三方 loader, 来编译 workers 脚本。

npm install worker-loader@3.0.8

接下来,修改 vue.config.js 文件:

// vue.config.js
module.exports = {
  chainWebpack(config) {
    config.module
      .rule('worker')
      .test(/\.worker\.js$/)
      .use('worker-loader')
      .loader('worker-loader')
      .options({})
      .end()  
  }
}

注意test() 设置了文件名后缀是 .worker.js 则为 worker 脚本文件

到这里第一个问题就解决了。。。


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

vue项目一改动到代码文件就会重新编译。

但在调试过程中,修改了 worker 脚本,发现一直没有修复到问题,一开始也是很怀疑自己是不是逻辑出错了。

通过 debug 才发现,代码一直没有修改。

后面每次修改 worker 脚本,都会重新启动 vue 项目,一开始问题是解决了。

但偶尔还是会没有修改到代码。

最终排查到:原来是每次重新编译时,要删除掉 node_modules 目录下的 .cache 文件夹
在这里插入图片描述

才会重新加载新 worker 脚本代码

问题3:主进程向子进程发送参数时,若参数存在对象,会报错

这里主要是生产 csvData 数据(key: value)中的 value 是一个对象结构时,发送给到 子进程,浏览器会报错。

这里解决方法是:将 value 进行序列化处理

// * 判断 csvData 中的值是否存在对象,需要序列化处理
const keys = csvHeader.map(item => item.key)
csvData = csvData.map(row => {
  return keys.reduce((acc, prev) => {
    acc[prev] = typeof row[prev] === 'object' ? JSON.stringify(row[prev]) : row[prev]
    return acc
  }, {})
})

问题4:在子进程中下载文件失败

主进程去结合实际业务逻辑生成 csvHeadercsvData 数据后,发送给到子进程,由其生成 Excel 文件流,并下载下来。

// 主进程
const { csvHeader, csvData } = generateExcelData(data)


// 子进程
import Excel from 'exceljs'
self.onmessage = async function(e) {
  const { csvData, csvHeader } = e.data

  const workbook = new Excel.Workbook()
  const worksheet = workbook.addWorksheet('My Sheet')

  worksheet.columns = csvHeader
  csvData.forEach(row => worksheet.addRow(row))

  // 生成 Excel 文件的 Buffer
  const excelBuffer = await workbook.xlsx.writeBuffer()

  // TODO 下载文件
}

经过调试发现文件下载不下来,查阅资料得出:

主要原因在于 Web Workers 的设计限制。具体来说,Web Workers 没有直接访问浏览器的 DOM 和一些与用户界面交互的功能,包括文件下载

所以这里只能将 Excel 文件的 Buffer转成blog发送给到主进程进行文件下载。
主进程

import { saveAs } from 'file-saver'
import ExportWorker from './export.worker.js'
const worker = new ExportWorker()
worker.postMessage({
  csvData: csvData,
  csvHeader: csvHeader
})

worker.onmessage = async(e) => {
  const { chunk: blog } = e.data
  saveAs(blog, filename)
}

worker 脚本

import Excel from 'exceljs'

self.onmessage = async function(e) {
  const { csvData, csvHeader } = e.data

  const workbook = new Excel.Workbook()
  const worksheet = workbook.addWorksheet('My Sheet')

  worksheet.columns = csvHeader
  csvData.forEach(row => worksheet.addRow(row))

  // 生成 Excel 文件的 Buffer
  const excelBuffer = await workbook.xlsx.writeBuffer()

  const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })

  self.postMessage({ chunk: blob })
}

源码

主进程

import { saveAs } from 'file-saver'
import ExportWorker from './export.worker.js'
/**
* 导出数据为 XLSX(通过 web Worker)
* @param {Object} csvHeader XLSX 头
* @param {Array} csvData 数据
* @param {String} filename 文件名
*/
const exportDataToXLSXByWorker = (csvHeader, csvData, filename) => {
    const worker = new ExportWorker()

    // * 判断 csvData 中的值是否存在对象,需要序列化处理
    const keys = csvHeader.map(item => item.key)
    csvData = csvData.map(row => {
      return keys.reduce((acc, prev) => {
        acc[prev] = typeof row[prev] === 'object' ? JSON.stringify(row[prev]) : row[prev]
        return acc
      }, {})
    })

    worker.postMessage({
      csvData: csvData,
      csvHeader: csvHeader
    })

    worker.onmessage = async(e) => {
      const { chunk: blog } = e.data
      saveAs(blog, filename)
    }
}

worker 脚本

import Excel from 'exceljs'

self.onmessage = async function(e) {
  const { csvData, csvHeader } = e.data

  const workbook = new Excel.Workbook()
  const worksheet = workbook.addWorksheet('My Sheet')

  worksheet.columns = csvHeader
  csvData.forEach(row => worksheet.addRow(row))

  // 生成 Excel 文件的 Buffer
  const excelBuffer = await workbook.xlsx.writeBuffer()

  const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })

  self.postMessage({ chunk: blob })
}

http://www.kler.cn/a/543830.html

相关文章:

  • IEEE期刊Word导出PDF注意事项
  • transformer
  • C++基础学习记录—this指针
  • deepseek + kimi 高效生成PPT
  • vue纯静态实现 视频转GIF 功能(附源码)
  • 没有服务器和显卡电脑如何本地化使用deepseek|如何通过API使用满血版deepseek
  • 【深入探讨 ResNet:解决深度神经网络训练问题的革命性架构】
  • HAL库框架学习总结
  • 产品详情页中 品牌官网详情 对应后端的字段是 detail
  • SpringBoot的单机模式是否需要消息队列?分布式应用中消息队列如何和服务的发现与注册、配置中心、SpringMVC相配合
  • unity 报错 Win32Exception: ApplicationName=‘git‘ 修复
  • ElementUI 抽屉组件高度封装
  • 图像处理技术和应用
  • 开源、免费项目管理工具比较:2025最新整理30款
  • 智慧物流新引擎:ARM架构工控机在自动化生产线中的应用
  • 2025年数据资产管理解决方案:资料合集,从基础知识到行业应用的全面解析
  • DeepSeek提问技巧总结
  • PhotoShop中创建窗口使用对应按钮创建对应图层简单示例
  • 业务开发 | 基础知识 | Maven 快速入门
  • 利用爬虫高效获取1688商品详情:案例指南
  • 【PHP的static】
  • 1.【BUUCTF】[极客大挑战 2019]PHP(反序列化)
  • 国际主流架构框架整理【表格版】简介、适用场景、优缺点、中文名、英名全称,附TOGAF认证介绍
  • 稀土抑烟剂——为汽车火灾安全增添防线
  • 基于ssm的超市订单管理系统
  • 【学习笔记】vue-cli中组件间传参的方式