logo

公共Hooks封装之报表导出useExportExcel

Nov 8, 2022 · 8 min

写在前面#

对于经常需要开发企业管理后台的前端开发来说,必不可少的需要使用表格对于数据进行操作,在对于现有项目进行代码优化时,封装一些公共的Hooks. 本篇文章为useExportExcel.js

基于个人项目环境进行封装的Hooks,仅以本文介绍封装Hooks思想心得,故相关代码可能不适用他人

项目环境#

Vue3.x + Ant Design Vue3.x + Vite3.x

对于企业管理后台最大的作用来说,用以管理企业内各种数据状况,同时,基于实际业务过程中,客户每逢年终(中)时都有大型汇报的需求,因此,数据报表形式的文档产出必不可少,本文则基于该常见需求场景进行封装的Hooks — 导出数据报表

封装思考:报表数据来源#

封装分解:前端生成报表#

接收options配置对象,包括data(源数据)、key(用来生成表格的行数据唯一标识)、title(表格标题)、fileName(导出文件名称)

// 通过数组数据前端导出excel
const exportByArray = options => {
  if (!options.data || !options.key || !options.title || !options.fileName)
  return new Error('缺少必需参数');
  const arr = options.data.map(v =>
    options.key.map(j => {
      return v[j];
    }),
  );
  arr.unshift(options.title);
  const ws = utils.aoa_to_sheet(arr);
  const colWidth = arr.map(row =>
    row.map(val => {
      if (val == null) {
        return { wch: 10 };
      } else if (val.toString().charCodeAt(0) > 255) {
        return { wch: val.toString().length * 2 };
      } else {
        return { wch: val.toString().length };
      }
    }),
  );
  const 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'];
      }
    }
  }
  ws['!cols'] = result;
  const wb = utils.book_new();
  utils.book_append_sheet(wb, ws, options.fileName);
  writeFile(wb, options.fileName + '.xlsx');
};

前端生成报表方法Sheet.js#

前端生成报表方法中用到的utils.aoa_to_sheetutils.book_newutils.book_append_sheetwriteFile,都来源于 SheetJS


Step1: 项目安装依赖yarn add xlsx

Step2: 在Hooks文件中引入 import { utils, writeFile } from 'xlsx'

Step3: 参考官方API,完善Hooks中前端导出方法 SheetJS - Utility Functions

这三种方法都是SheetJS的导出方法,存在差异,考虑实际数据,最后选择的是utils.aoa_to_sheet,其余方法可以在官方文档中找到对应的示例


以上是一个完整的导出报表流程
utils.book_new => 创建一个工作簿 utils.aoa_to_sheet => 源数据转成工作表 utils.book_append_sheet => 将工作表插入到工作簿中 writeFile => 调用下载

封装分解:后端接口返回数据导出优化#

因为需要请求后端接口导出,即下载返回的二进制文件,依旧考虑用户体验设计,增加二次确认弹窗,并从store里拿接口必须的token

// 打开导出文件确认弹窗
const exportByResBlob = options => {
  Modal.confirm({
    title: options.title ? options.title : '导出确认',
    content: options.content ? options.content : '确认导出报表吗?',
    onOk() {
      downloadFile(options);
      return Promise.resolve();
    },
  });
};

useExportExcel.js完整代码#

import { onBeforeUnmount } from 'vue';
import { utils, writeFile } from 'xlsx';
import { stringify } from 'qs';
import { Modal } from 'ant-design-vue';
import { useUserStore } from '@/store/userStore';

export function useExportExcel() {
  const userStore = useUserStore();
  let xhr = null;
  let downloading = false; // 限制同一文件同时触发多次下载

  onBeforeUnmount(() => {
    xhr && xhr.abort();
  });

  // 打开导出文件确认弹窗
  const exportByResBlob = options => {
    Modal.confirm({
      title: options.title ? options.title : '导出确认',
      content: options.content ? options.content : '确认导出报表吗?',
      onOk() {
        downloadFile(options);
        return Promise.resolve();
      },
    });
  };

  // 通过请求后端接口文件流导出excel
  const downloadFile = options => {
    try {
      if (downloading || !options.url || !options.fileName)
      return new Error('缺少必需参数');
      downloading = true;
      const paramsStr = stringify(options.params || {});
      xhr = new XMLHttpRequest();
      xhr.responseType = 'blob';
      if (paramsStr) {
        xhr.open('get', `${options.url}?${paramsStr}`, true);
      } else {
        xhr.open('get', options.url, true);
      }
      xhr.setRequestHeader('token', userStore.userToken);
      xhr.onloadend = function (e) {
        if (e.target.status === 200) {
          const aElement = document.createElement('a');
          const blob = e.target.response;
          const url = window.URL.createObjectURL(blob);
          aElement.style.display = 'none';
          aElement.href = url;
          aElement.download = `${options.fileName}.xlsx`;
          document.body.appendChild(aElement);
          aElement.click();
          if (window.URL) {
            window.URL.revokeObjectURL(blob);
          } else {
            window.webkitURL.revokeObjectURL(blob);
          }
          document.body.removeChild(aElement);
          downloading = false;
        }
      };
      xhr.send();
    } catch (e) {
      console.error(e);
      downloading = false;
      Modal.error({
        title: '提示',
        content: '导出发生异常,请重试',
      });
    }
  };

  // 通过数组数据前端导出excel
  const exportByArray = options => {
    if (!options.data || !options.key || !options.title || !options.fileName) return new Error('缺少必需参数');
      const arr = options.data.map(v =>
        options.key.map(j => {
          return v[j];
        }),
      );
      arr.unshift(options.title);
      const ws = utils.aoa_to_sheet(arr);
      const colWidth = arr.map(row =>
      row.map(val => {
        if (val == null) {
          return { wch: 10 };
        } else if (val.toString().charCodeAt(0) > 255) {
          return { wch: val.toString().length * 2 };
        } else {
          return { wch: val.toString().length };
        }
      }),
    );
    const 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'];
        }
      }
    }
    ws['!cols'] = result;
    const wb = utils.book_new();
    utils.book_append_sheet(wb, ws, options.fileName);
    writeFile(wb, options.fileName + '.xlsx');
  };

  return {
    exportByResBlob,
    exportByArray,
  };
}
> cd ..