LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

【Web开发】前端及后端导出Excel功能实现指南

admin
2025年12月10日 21:37 本文热度 8
一、需求分析:列表数据导出的必要性

在B端系统中,导出能是用户日常工作中不可或缺的功能。用户经常需要将业务数据、统计信息等为Excel表格,用于数据分析报表生成存档。随着系统的推广使用,数据量不断增长,如何高效、稳定地实现大数据量的Excel导出成为了我们面临的重要挑战。

1.1 初始方案:前端直接导出实现

在系统开发初期,考虑到数据量相对较小(通常在几千条以内),我们选择了前端直接导出的方案。这种方案实现简单,无需后端特殊处理,用户体验也较为流畅。

核心实现逻辑如下:

  1. 一次性获取所有数据:通过API请求获取符合条件的所有数据(设置较大的pageSize)
  2. 前端数据处理:对获取的数据进行格式化和转换
  3. 调用Excel生成库:使用专门的前端库将数据转换为Excel文件
  4. 触发浏览器下载:将生成的Excel文件提供给用户下载

具体代码实现:

// 最初版本的Excel导出实现oldExportToExcel() {  let _this = this  // 显示加载状态  _this.exportLoading = true
  // 一次性获取所有数据  const url = _this.jUrl.list  request.get(url, {    ..._this.queryParam,    pageNo1,    pageSize10000// 设置一个较大的值尝试获取所有数据  }).then(response => {    if (response.success && response.result && response.result.records) {      // 数据格式化处理      const data = this.formatExportData(response.result.records)            // 生成并下载Excel文件      try {        const { export_json_to_excel } = require('@/vendor/Export2Excel')        export_json_to_excel(          _this.tHeader,  // 表头          data,          // 数据内容          _this.title,   // 文件名          []             // 合并单元格配置        )      } catch (error) {        console.error('导出失败:', error)        this.$message.error('导出失败,请重试')      } finally {        // 隐藏加载状态        _this.exportLoading = false      }    } else {      _this.exportLoading = false      this.$message.warning('暂无数据可导出')    }  }).catch(error => {    console.error('数据获取失败:', error)    _this.exportLoading = false    this.$message.error('数据获取失败,请重试')  })}// 数据格式化处理函数(示例代码,字段名称需根据具体业务调整)formatExportData(dataList) {  // 简单的数据格式化  return dataList.map(item => {    // 仅做简单转换,不做复杂处理    // 注:以下字段为示例,实际项目中应根据业务需求调整    return {      id: item.id || '',      name: item.name || '',      contact: item.phone || '',      email: item.email || '',      businessField: item.positionName || '',      status: item.statusName || '',      createTime: item.createTime || ''    }  })}

1.2 Export2Excel库的实现原理

在前端导出方案中,我们使用了@/vendor/Export2Excel这个核心库来处理Excel文件的生成。该库基于多个开源工具构建,提供了完整的Excel导出功能。

核心依赖:

  • xlsx.js:负责将数据转换为Excel文件格式
  • file-saver.js:处理文件下载功能
  • Blob.js:提供二进制大对象处理能力,确保在不同浏览器中兼容性

主要实现流程:

  1. 数据转换:将JSON格式数据转换为Excel工作表(worksheet)结构
  2. 样式设置:为表头和内容设置字体、颜色、对齐方式等样式
  3. 列宽自适应:根据内容长度自动计算并设置列宽,特别处理中文字符
  4. 合并单元格:支持复杂的单元格合并需求
  5. 文件生成:将工作表打包成完整的工作簿(workbook)并生成二进制数据
  6. 触发下载:使用Blob和file-saver实现Excel文件的下载

核心函数:

// 将JSON数据转换为适合Excel的二维数组function format_json(filterVal, jsonData) {  // 将JSON对象数组转换为按指定字段排列的二维数组}// 导出Excel文件的主函数export function export_json_to_excel(header, data, filename, merges) {  // 1. 准备数据(添加表头)  // 2. 创建工作表和工作簿  // 3. 设置单元格样式  // 4. 处理合并单元格  // 5. 自动调整列宽  // 6. 生成二进制Excel数据  // 7. 触发文件下载}// 将字符串转换为ArrayBufferexport function s2ab(s) {  // 用于将Excel数据转换为可下载的二进制格式}

1.3 性能瓶颈:小数据方案的局限性

随着系统用户量的增长和数据的累积,当初始方案面对大量数据时(特别是当需要导出数千甚至数万人的候选人信息时),逐渐暴露出严重的性能问题:

  1. 页面卡顿甚至崩溃:大量数据处理会阻塞JavaScript主线程,导致UI无响应
  2. 内存占用过高:一次性加载和处理大量数据会导致浏览器内存急剧上升,在低配置设备上尤为明显
  3. 请求超时:单次请求数据量过大容易导致服务器超时,特别是在网络环境不稳定的情况下
  4. 用户体验差:用户需要长时间等待,没有任何进度反馈,无法判断导出是否成功或何时完成

这些问题促使我们必须重新思考和优化Excel导出方案,以满足日益增长的数据量需求和用户体验要求。

二、解决方案:前端优化与后端导出的智能分流

面对大数据量导出的挑战,我们采取了"双管齐下"的策略:一方面对前端导出进行性能优化,另一方面引入后端导出作为补充方案。通过智能分流机制,根据数据量和使用场景自动选择最适合的导出方式。

2.1 前端优化:Web Worker助力大数据处理

对于中等规模的数据量(通常在1万条以内),我们对前端导出流程进行了全面重构,具体webWorker实现可查看上一篇文章大数据处理的前端性能优化方案

核心优化思路如下:

  1. 数据分片获取:将大数据量请求拆分为多个小批量请求,避免单次请求数据过大
  2. 并行处理:使用Web Worker在后台线程并行处理数据,不阻塞主线程
  3. 渐进式数据合并:逐步收集和合并数据,避免一次性加载全部数据到内存

2.2 后端导出:超大批量数据的终极解决方案

对于超大批量数据(通常在10万条以上),我们引入了后端导出方案,其核心实现包括:

  1. 异步任务处理:将导出任务提交到后台队列,通过异步方式处理
  2. 服务器端生成:在服务器端直接生成Excel文件,避免前端资源消耗
  3. 文件存储与下载:生成的文件存储在服务器,提供下载链接给用户
  4. 任务状态通知:通过WebSocket或定时轮询,向用户反馈导出进度和结果

后端导出实现流程图:

2.2 Web Worker实现

下面是我们的Worker.js核心实现代码:

self.onmessage = function (e) {  // 监听主线程发过来的消息  let data = JSON.parse(JSON.stringify(e.data))  self.url = data.url  self.headers = data.header  self.params = data.params  self.startPage = data.startPage  self.endPage = data.endPage  // 拼接请求参数  let paramsArray = []  Object.keys(self.params).forEach((key) =>    paramsArray.push(key + '=' + self.params[key])  )  if (self.url.search(/\?/) === -1) {    self.url += '?' + paramsArray.join('&')  } else {    self.url += '&' + paramsArray.join('&')  }  // 封装fetch请求  self.fetchRequest = async (pageNo) => {    let url = `${self.url}&pageNo=${pageNo}`    return new Promise((resolve, reject) => {      fetch(url, {        method'get',        headers: self.headers,      })        .then((res) => res.json())        .then((res) => {          resolve(res)        })        .catch((err) => {          reject(err)        })    })  }  // 获取指定范围内的所有数据  self.getList = async () => {    let list = []    for (let i = self.startPage; i <= self.endPage; i++) {      let { success, result } = await self.fetchRequest(i)      if (success && result && result.records && result.records.length) {        list = list.concat(result.records)      }    }    self.postMessage({ list }) // 将结果发送回主线程  }  self.getList()}

2.3 主线程实现

在主线程中,我们实现了智能的数据获取策略选择和Worker管理:

self.onmessage = function (e) {  // 监听主线程发过来的消息  let data = JSON.parse(JSON.stringify(e.data))  self.url = data.url  self.headers = data.header  self.params = data.params  self.startPage = data.startPage  self.endPage = data.endPage  // 拼接请求参数  let paramsArray = []  Object.keys(self.params).forEach((key) =>    paramsArray.push(key + '=' + self.params[key])  )  if (self.url.search(/\?/) === -1) {    self.url += '?' + paramsArray.join('&')  } else {    self.url += '&' + paramsArray.join('&')  }  // 封装fetch请求  self.fetchRequest = async (pageNo) => {    let url = `${self.url}&pageNo=${pageNo}`    return new Promise((resolve, reject) => {      fetch(url, {        method'get',        headers: self.headers,      })        .then((res) => res.json())        .then((res) => {          resolve(res)        })        .catch((err) => {          reject(err)        })    })  }  // 获取指定范围内的所有数据  self.getList = async () => {    let list = []    for (let i = self.startPage; i <= self.endPage; i++) {      let { success, result } = await self.fetchRequest(i)      if (success && result && result.records && result.records.length) {        list = list.concat(result.records)      }    }    self.postMessage({ list }) // 将结果发送回主线程  }  self.getList()}
// 导出方法入口exportFile({  title,  tHeader,  filterVal,  merges = [],  idsType = 'string',  queryIds = [],}) {  let _this = this  let ids = this.selectedRowKeys  if (queryIds.length > 0) {    ids = queryIds  }  require.ensure([], async () => {    _this.exportFilterVal = filterVal    _this.exportMerges = merges    _this.exportTitle = title    _this.exportTHeader = tHeader    let list = []    _this.exportLoading = true    if (ids && ids.length) {      // 导出选中项逻辑      const url = _this.jUrl.list      const data = await request.get(url, {        id: idsType === 'string' ? ids.join(',') : ids,        pageNo1,        pageSize: ids.length + 10,        column'createTime',        order'desc',      })      list = data.result        ? data.result.records          ? data.result.records          : data.result        : []      _this.exportByList(list)    } else {      // 根据数据量大小选择不同的策略      let currentPage = Math.ceil(_this.pagination.total / 1000)      let totalPage = currentPage > 100 ? 100 : currentPage      if (totalPage >= 10) {        // 大数据量使用Web Worker        _this.excelDataInfoByworker(totalPage)      } else {        // 小数据量直接获取        for (let i = 0; i < totalPage; i++) {          const temp = await _this.excelDataInfo(i + 11000)          list = list.concat(temp)        }        _this.exportByList(list)      }    }  })},// Web Worker并行处理大数据量excelDataInfoByworker(totalPage) {  let _this = this  this.exportWorkerIndex = 0  this.exportList = []  let url = process.env.VUE_APP_API_BASE_URL + '/' + this.jUrl.list  let header = this.tokenHeader  let params = {    url,    header,    paramsObject.assign(      this.queryParam,      {        pageSize1000,      },      this.otherQueryParams    ),  }  // 最多创建10个Worker并行处理  let pages = Math.floor(totalPage / 10)  for (let i = 0; i < 10; i++) {    let worker1 = new Worker(new URL('./worker.js'import.meta.url))    let params1 = JSON.parse(JSON.stringify(params))    params1.startPage = i * pages + 1    if (i == 9) {      params1.endPage = totalPage    } else {      params1.endPage = (i + 1) * pages    }    // 向Worker发送任务    worker1.postMessage(params1)    // 接收Worker返回的结果    worker1.onmessage = (e) => {      _this.exportWorkerIndex++      if (e.data && e.data.list && e.data.list.length > 0) {        _this.exportList = _this.exportList.concat(e.data.list)      }      // 所有Worker完成后执行导出      if (_this.exportWorkerIndex == 10) {        _this.exportByList(_this.exportList)      }      // 释放Worker资源      worker1.terminate()    }  }}

三、Excel生成与下载实现

完成数据获取后,我们使用xlsx库生成Excel文件并提供下载功能:

// 数据格式化export function format_json(filterVal, jsonData) {  return jsonData.map((v) =>    filterVal.map((j) => {      const jAarr = j.split('.')      const jLength = jAarr.length      if (v[jAarr[0]] || v[jAarr[0]] == 0) {        if (jLength === 1) {          return String(v[jAarr[0]])        } else if (jLength === 2) {          if (v[jAarr[0]][jAarr[1]]) {            return String(v[jAarr[0]][jAarr[1]])          } else {            return ''          }        }      } else {        return ''      }    })  )}// 导出Excel文件export function export_json_to_excel(header, data, filename, merges) {  filename = filename || 'excel-list'  data = [...data]  data.unshift(header)  var ws_name = 'Sheet1'  var wb = new Workbook(),    ws = sheet_from_array_of_arrays(data)  // 设置表头和内容样式  for (var i = 0; i < header.length; i++) {    var str = String.fromCharCode(65 + i) //A B C D....    //设备表头样式    var head = str + '1'    if (ws[head]) {      ws[head].s = {        font: { sz12boldtruecolor: { rgb'000000' } },        alignment: { vertical'center'horizontal'center' },        fill: { bgColor: { indexed64 }, fgColor: { rgb'CCCCCC' } },      }    }    //设置内容样式    for (let j = 2; j < data.length; j++) {      var body = str + j      if (str == 'D') {        if (ws[body]) {          ws[body].s = {            font: { sz12 },            alignment: {              vertical'center',              horizontal'left',              wrapText1,              indent0,            },          }        }      } else {        if (ws[body]) {          ws[body].s = {            font: { sz12 },            alignment: {              vertical'center',              horizontal'center',              wrapText1,              indent0,            },          }        }      }    }  }  // 合并单元格处理  if (merges) {    ws['!merges'] = merges  }  // 自动调整列宽  let autoWidth = true  if (autoWidth) {    /*设置worksheet每列的最大宽度*/    const colWidth = data.map((row) =>      row.map((val) => {        /*先判断是否为null/undefined*/        if (val == null) {          return {            wch10,          }        } else if (val.toString().charCodeAt(0) > 255) {          return {            wch: val.toString().length * 2,          }        } else {          return {            wch: val.toString().length,          }        }      })    )    /*以第一行为初始值*/    let result = colWidth[0]    for (let i = 1; i < colWidth.length; i++) {      for (let j = 0; j < colWidth[i].length; j++) {        if (result[j]['wch'] < colWidth[i][j]['wch']) {          result[j]['wch'] = colWidth[i][j]['wch']        }      }    }    //最大不超过200    result.forEach((resultItem) => {      if (resultItem.wch > 150) {        resultItem.wch = 150      }    })    ws['!cols'] = result  }  // 生成并下载Excel文件  wb.SheetNames.push(ws_name)  wb.Sheets[ws_name] = ws  var wbout = XLSX.write(wb, {    bookType'xlsx',    bookSSTfalse,    type'binary',  })  saveAs(    new Blob([s2ab(wbout)], {      type'application/octet-stream',    }),    filename + '.xlsx'  )}// 将字符串转换为ArrayBufferexport function s2ab(s) {  var buf = new ArrayBuffer(s.length)  var view = new Uint8Array(buf)  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff  return buf}

四、实现效果

通过这些优化,我们成功解决了大数据量导出的问题,实现了以下效果:

  1. 页面不再卡顿:Web Worker在后台处理数据,主线程保持响应
  2. 内存占用优化:分批加载和处理数据,避免一次性加载全部数据
  3. 导出速度提升:多线程并行处理大幅提升了大数据量的导出速度
  4. 稳定性增强:避免了因数据量过大导致的浏览器崩溃问题

五、功能完善与后续优化

随着系统的使用,我们还进行了以下完善:

5.1 导出前数据处理

增加了数据预处理功能,确保导出数据的准确性和一致性:

async excelDataInfo(pageNo, pageSize) {  let queryParam = this.handleQueryParam()  const obj = Object.assign(    queryParam,    {      pageNo,      pageSize,    },    this.otherQueryParams  )  const url = this.jUrl.list  const data = await request.get(url, obj)  let result = []  if (data && data.result && data.result.records) {    // 数据预处理,确保导出数据的准确性    data.result.records.forEach((item) => {      // 注:以下字段处理为示例,实际项目中应根据业务需求调整      if (item.age) {        item.age = String(item.age) // 年龄字段处理      }      if (item.isLongValid == '1') {        item.certificateEndDate = '长期' // 证书有效期特殊处理      }    })    result = data.result.records  }  return result}

5.2 用户体验优化

添加了加载状态提示,让用户了解导出进度:

exportByList(list) {  console.log('导出', list.length)  try {    const {      export_json_to_excel,      format_json,    } = require('@/vendor/Export2Excel')    const data = format_json(this.exportFilterVal, list)    export_json_to_excel(      this.exportTHeader,      data,      this.exportTitle,      this.exportMerges    )    setTimeout(() => {      this.exportLoading = false    }, 1500)  } catch (error) {    console.log(error)  }}

5.3 未来优化方向

尽管我们已经取得了显著的优化效果,但仍有改进空间:

  1. Worker池管理:实现Worker池机制,避免频繁创建和销毁Worker实例
  2. 进度反馈增强:提供更精确的导出进度反馈,如百分比显示
  3. 流式处理:对于超大批量数据,实现真正的流式处理,进一步降低内存占用
  4. 错误处理增强:完善Worker中的错误处理机制,提供更好的容错能力
  5. 导出队列:实现导出任务队列,避免同时进行多个大数据量导出操作

六、前端导出与后端导出:场景选择与最佳实践、

在我们的项目中,我们同时使用了前端导出和后端导出两种方案,它们各自有不同的适用场景和优势。下面是对两种方案的详细对比和选择建议:

6.1 前端导出的适用场景

前端导出(包括优化后的Web Worker方案)适用于以下情况:

  1. 数据量适中:通常适用于1万条以内的数据导出
  2. 实时性要求高:用户需要立即获取导出结果
  3. 个性化需求强:需要根据用户操作动态生成导出内容
  4. 网络环境良好:网络带宽充足,延迟较低
  5. 服务器资源紧张:希望减轻服务器负担

前端导出的优势:

  • 实现简单,无需后端复杂处理
  • 响应迅速,用户无需长时间等待
  • 交互灵活,可以根据用户选择动态调整导出内容

6.2 后端导出的适用场景

后端导出适用于以下情况:

  1. 超大批量数据:通常适用于10万条以上的数据导出
  2. 数据处理复杂:需要进行复杂的数据计算、汇总或跨系统数据整合
  3. 格式要求严格:需要严格按照特定格式或规范生成Excel文件
  4. 稳定性要求高:不希望因前端环境差异导致导出失败
  5. 离线处理需求:用户可以提交任务后离开,稍后再来获取结果

后端导出的优势:

  • 不受前端资源限制,可以处理超大批量数据
  • 稳定性更高,不受浏览器和设备性能影响
  • 可以实现更复杂的数据处理和格式定制
  • 支持异步处理和任务排队机制

6.3 智能分流策略

在实际应用中,我们采用了智能分流策略,根据不同情况自动选择最合适的导出方式:

  1. 小数据量(<5000条):直接使用前端简单导出
  2. 中等数据量(5000-10000条):使用Web Worker优化的前端导出
  3. 大数据量(>10000条):自动切换到后端导出方案

6.4 优化经验总结

通过这次Excel导出功能的优化过程,我们总结出以下经验:

  1. 分析问题根源:面对性能瓶颈,首先要深入分析问题的根本原因,而不是简单地增加硬件资源
  2. 技术选型要匹配场景:不同的技术有不同的适用场景,要根据实际需求选择最合适的方案
  3. 组合多种技术方案:单一技术往往难以应对复杂场景,组合多种技术可以达到最佳效果
  4. 用户体验优先:无论采用哪种方案,都要确保用户有良好的体验,如进度反馈、错误处理等
  5. 持续监控和优化:系统上线后,要持续监控性能表现,并根据实际运行情况进行优化

6.5 结语

Excel导出看似简单,但在大数据量场景下却面临诸多挑战。通过前端优化与后端导出的智能结合,我们成功解决了这一难题,为用户提供了稳定、高效的数据导出体验。

在Web应用开发中,面对类似的性能挑战,我们应该保持开放的思维,综合运用各种技术手段,以用户体验为中心,不断优化和完善系统功能。只有这样,才能打造出既功能强大又用户友好的高质量Web应用。


阅读原文:原文链接


该文章在 2025/12/11 8:52:07 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved