关于前端导出

为什么要做前端导出?

  1. 之前的产品前端交互是不分页,全列表展示。数据前端都有,前端导出不消耗服务器性能
  2. 数据数据量大,后端导出经常出现内存溢出情况

需求一:用户需要导出30W+数据量

前面说过,用户的数据量比较大,又由于是全量数据展示【这里已经做过数据结构优化和压缩】。30W+数据配合虚拟表格前端渲染压力不大,压力在于页面第一次加载的时候比较慢。

那我们直接把数据从内存中转成Blob对象导出也就可以了。

问题来了,前端展示的数据存在加密数据,需要解密导出。纳尼,又要重新去拿解密之后的数据。

行吧,把30W条数据分批次去获取解密数据,每次1000条,一共300个请求。很好,服务器直接打挂了。

加个请求队列吧!!!控制下最大并发数。让它每次最多发6个,当前队列中有请求回来之后立即执行后续队列中请求。

javascript 复制代码
export default class AsyncRequestQueue {
  private maxConcurrency: number;
  private queue: Array<{
    request: () => Promise<any>;
    resolve: (value: unknown) => void;
    reject: (value: unknown) => void;
    index: number;
    processed: boolean;
  }>;
  private activeCount: number;
  private results: Array<any>;
  private nextPromiseResolver: any;
  constructor(maxConcurrency) {
    this.maxConcurrency = maxConcurrency;
    this.queue = [];
    this.activeCount = 0;
    this.results = [];
    this.nextPromiseResolver = null;
  }

  addRequest(request) {
    return new Promise((resolve, reject) => {
      const requestObj = {
        request,
        resolve,
        reject,
        index: this.queue.length,
        processed: false,
      };
      this.queue.push(requestObj);
      this.processQueue();
    });
  }

  processQueue() {
    if (this.activeCount >= this.maxConcurrency) {
      return;
    }

    const requestObj = this.queue.find(req => !req.processed);
    if (!requestObj) {
      if (this.activeCount === 0 && this.nextPromiseResolver) {
        // 所有请求都已完成
        this.nextPromiseResolver(this.results);
      }
      return;
    }

    const { request, resolve, reject, index } = requestObj;
    requestObj.processed = true;
    this.activeCount++;
    request()
      .then(response => {
        this.results.push({ index, response });
        resolve(response);
      })
      .catch(error => {
        reject(error);
      })
      .finally(() => {
        this.activeCount--;
        this.processQueue();
      });
  }

  execute() {
    return new Promise(resolve => {
      this.nextPromiseResolver = resolve;
      this.processQueue();
    });
  }
}
javascript 复制代码
const requestQueue = new AsyncRequestQueue(6);
let selectedKeysChunks = chunk(selectedKeys, MAX_BATCH_TOTAL); // 每1000条数据一个请求批次
promises = selectedKeysChunks.map(selectedKeys => () =>
    request<{ data: any }, any>(QUERY_WORK_ORDER_LIST_V3, {}).then(res => res?.data || []))
promises.map(item => requestQueue.addRequest(item));
const list = await requestQueue
    .execute()
    .then(responses => {
      // 按照输入队列的顺序返回请求结果
      const orderedResponses = (responses as any[]).sort(
        (a, b) => a.index - b.index,
      );
      return orderedResponses.map(res => res.response);
    })
    .catch(error => {
      console.error('Error occurred during request queue:', error);
    });
  

需求二:列头需要支持多级列头/合并与拆分

表格中的数据都是来自于自定义组件,自定义组件的结构可能是多层级的,所以展示的时候,有的需要合并在一起,有的需要单独拆开进行导出。

  • 先通过exceljs建立一个workbook 实例对象
  • 列头数据格转换
  • 计算列头最大深度,有的列头是3级,有的列头是4级,需要用最深的列头作为标准
  • 计算生成Cell单元
  • 左右比对,上下比对,如果发现key一致的Cell单元进行合并
typescript 复制代码
// TODO: fields 结构为[{title: "列头", dataIndex: 'col1', children: []}]
export interface ITableHeader {
  header: string;
  // 用于数据匹配的 key
  key: string;
  // 列宽
  width: number;
  // 父级的 key
  parentKey?: string;
  children?: ITableHeader[];
}

function generateHeaders(columns: any[], parentIndex?) {
  return columns?.map(col => {
    const obj: ITableHeader = {
      // 显示的 name
      header: col.title,
      // 用于数据匹配的 key
      key: col.dataIndex,
      // 列宽
      width: DEFAULT_COLUMN_WIDTH,
    };
    if (parentIndex) {
      obj.parentKey = parentIndex;
    }
    if (Array.isArray(col.children)) {
      obj.children = generateHeaders(col.children, col.dataIndex);
    }
    return obj;
  });
}

function calculateMaxDepth(columns: Array<Header>) {
  let maxDepth = 1;

  function calculateDepth(column, depth) {
    if (column.children) {
      for (const child of column.children) {
        calculateDepth(child, depth + 1);
      }
      maxDepth = Math.max(maxDepth, depth + 1);
    }
  }

  for (const column of columns) {
    calculateDepth(column, 1);
  }

  return maxDepth;
}

// 计算组件所有叶子节点个数
const getMaxChildLength = node => {
  if (!node.children || node.children.length === 0) {
    return 1; // 当前节点是叶子节点
  }

  let count = 0;
  for (const childNode of node.children) {
    count += getMaxChildLength(childNode);
  }

  return count;
};


const loopPushCell = (
  column,
  rowArray: Array<Array<{ title: string; dataIndex: string }>> = [[]],
  index = 0,
  maxDepth = 0,
) => {
  if (index >= maxDepth) return;
  const count = getMaxChildLength(column);
  const columns = new Array(count)?.fill(null)?.map(() => ({
    title: column.title,
    dataIndex: column.dataIndex,
  }));
  rowArray[index].push(...columns);
  // 有子节点的时候,继续
  if (Reflect.has(column, 'children')) {
    ++index;
    column.children?.map(item => loopPushCell(item, rowArray, index, maxDepth));
  } else {
    // 无子节点的时候,子节点的数据继承父节点数据
    loopPushCell(column, rowArray, ++index, maxDepth);
  }
};

// 列头单行合并
export const mergeColumnsCell = (
  rowData: Array<{ title: string; dataIndex: string }>,
  worksheet: Worksheet,
  rowNumber: number,
  rowInstance: Row,
) => {
  // 当前列指针
  let pointer = -1;
  rowData.forEach((cellData, index) => {
    if (index <= pointer) return;
    pointer = lastIndexOfObject(rowData, cellData);
    if (index !== pointer) {
      const START = getExcelAlpha(index + 1);
      const END = getExcelAlpha(pointer + 1);
      console.debug(
        'mergeColumns',
        Number(rowNumber),
        index + 1,
        Number(rowNumber),
        pointer + 1,
        `${START}${Number(rowNumber) + 1}:${END}${Number(rowNumber) + 1}`,
      );
      worksheet.mergeCells(
        `${START}${Number(rowNumber) + 1}:${END}${Number(rowNumber) + 1}`,
      );
      const cell = rowInstance.getCell(index + 1);
      cell.alignment = {
        vertical: 'middle',
        horizontal: 'center',
        wrapText: true,
      };
    }
  });
};

// 矩阵反转
export function transposeMatrix<T>(matrix: Array<Array<T>>) {
  const numRows = matrix.length;
  const numCols = matrix[0].length;
  const result: Array<Array<T>> = [];

  for (let col = 0; col < numCols; col++) {
    const newRow: Array<T> = [];
    for (let row = 0; row < numRows; row++) {
      newRow.push(matrix[row][col]);
    }
    result.push(newRow);
  }

  return result;
}


function drawExcelHeader(
  worksheet: Worksheet,
  headerRows: Array<Array<{ title: string; dataIndex: string }>>,
) {
  // 行处理
  headerRows?.map((row, index) => {
    // 获取单行列头所有名称
    const columnsName = row?.map(columns => columns.title);
    // 单行列头插入
    const rowHeader = worksheet.addRow(columnsName);
    // 样式修改
    addHeaderStyle(rowHeader, { color: 'dff8ff' });
    // 行合并列头
    mergeColumnsCell(row, worksheet, index, rowHeader);
  });
  if (headerRows?.length > 1) {
    // 二维数组反转
    const transList = transposeMatrix(headerRows);
    transList?.map((row, index) => {
      // 列处理
      mergeRowCells(row, worksheet, index);
    });
  }
}

 // 创建工作簿
const workbook = new ExcelJs.Workbook();
// 添加sheet
const worksheet = workbook.addWorksheet(fileName);
// 设置 sheet 的默认行高
worksheet.properties.defaultRowHeight = 20;
// 解析 columns
const headers = generateHeaders(fields);
// 计算当前Excel列头最大深度
const maxDepth = calculateMaxDepth(fields);
// 根据最大深度,生成需要的二维数组,每个元素对应一个Cell格
  const excelHeaderRow: Array<Array<{
    title: string;
    dataIndex: string;
  }>> = new Array(maxDepth).fill(null).map(() => []);
  fields?.map(item => loopPushCell(item, excelHeaderRow, 0, maxDepth));

drawExcelHeader(worksheet, excelHeaderRow);

需求三:图片导出

用户说针对图片类型的数据,我不想在Excel中展示链接,我想看图片。。。

  • 先从列头中找到哪些列是图片,这里列头里面有组件类型,可以根据组件类型来找到
  • 找出每一条数据中最大图片长度,根据最大长度扩展列头长度
  • 为列头增加dataIndex
  • 图片写入
typescript 复制代码
// TODO: 列头计算
const formatPictures = pictures => {
  if (!pictures) return [];
  pictures = pictures.replace(/[\[\]]/g, '');
  pictures = pictures.replace(/\s/g, '');
  pictures = pictures ? pictures.split(',') : [];
  return pictures.map(
    item => `https://kefu.kuaimai.com/${item}?x-oss-process=image/resize,l_500`,
  );
};

export const exportPicture = ({ columns, dataSource, filterColumns }) => {
  const pictures = columns.filter(column => column.type === 'PICTURE');
  if (pictures?.length > 0) {
    // 获取所有的图片组件的组件id
    const picturesKeys = pictures.map(item => `${item.dataIndex}_picture`);
    // 为每个图片组件确认图片个数
    let uniKeyMaxPic: picturesKeysType = picturesKeys.reduce((cur, nxt) => {
      cur[nxt] = 1;
      return cur;
    }, {});
    uniKeyMaxPic = dataSource.reduce((cur, nxt) => {
      Object.keys(cur).map(i => {
        cur[i] = Math.max(cur[i], formatPictures(nxt[i])?.length);
      });
      return cur;
    }, uniKeyMaxPic);
    // 采集最大单行图片个数
    const maxPicture = Object.values(uniKeyMaxPic).reduce(
      (cur: number, nxt) => {
        return (cur += Number(nxt));
      },
      0,
    );
    if (maxPicture > 0) {
      // 开始对导出列头新增图片列
      filterColumns = filterColumns.filter(
        item => !picturesKeys.includes(`${item.dataIndex}_picture`),
      );
      const finalUniKeyMaxPic = Object.keys(uniKeyMaxPic).reduce((cur, nxt) => {
        return {
          ...cur,
          [nxt]: {
            dataIndex: new Array(uniKeyMaxPic[nxt])
              .fill('')
              .map(item => uuidv4()),
            title: columns.find(c => nxt?.includes(c.dataIndex))?.name,
          },
        };
      }, {});
      let flatColumns = [];
      Object.keys(finalUniKeyMaxPic).map(nxt => {
        if (finalUniKeyMaxPic[nxt].dataIndex?.length > 0) {
          flatColumns = flatColumns.concat(
            finalUniKeyMaxPic[nxt].dataIndex.map((i, index) => ({
              title: finalUniKeyMaxPic[nxt].title,
              dataIndex: i,
              uniqueKey: nxt,
              idx: index,
            })),
          );
        }
      });
      filterColumns = filterColumns.concat(flatColumns);
      // 对导出数据中图片的内容进行格式化
      dataSource = dataSource.map(item => {
        const newItem = {};
        flatColumns.map(
          (i: {
            dataIndex: string;
            title: string;
            uniqueKey: string;
            idx: number;
          }) => {
            newItem[i.dataIndex] = formatPictures(item[i.uniqueKey])?.[i.idx];
          },
        );
        return {
          ...item,
          ...newItem,
        };
      });
      return {
        columns: filterColumns,
        dataSource: dataSource,
      };
    }
  }
  return {
    columns,
    dataSource,
  };
};

// 图片写入
const getBase64Image = img => {
  let canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  let ctx = canvas.getContext('2d');
  ctx?.drawImage(img, 0, 0, img.width, img.height);
  let ext = img.src.substring(img.src.lastIndexOf('.') + 1).toLowerCase();
  let dataURL = canvas.toDataURL('image/' + ext);
  return dataURL;
};

const getBase64 = (url: string) => {
  return new Promise((resolve, reject) => {
    let image = new Image();
    image.crossOrigin = 'anonymous';
    image.src = url;
    image.onload = () => resolve(getBase64Image(image));
    image.onerror = error => reject(error);
  });
};

async function addImage2Cell(workbook, worksheet, url, row, col) {
  try {
    const myBase64Image = await getBase64(url);
    const imageId = workbook.addImage({
      base64: myBase64Image,
      extension: 'png',
    });
    worksheet.addImage(imageId, {
      tl: { col: col, row: row },
      br: { col: col + 1, row: row + 1 },
    });
  } catch (e) {
    console.error('图片解析失败', url);
  }
}


async function addImages2table({
  workbook,
  worksheet,
  fields,
  dataSource,
  headerKeys,
  offsetRow,
}) {
  let picNumber = 0;
  const colNums = fields.reduce(
    (
      cur: { index: number; dataIndex: string }[],
      nxt: { uniqueKey?: string },
      index: number,
    ) => {
      if (Reflect.has(nxt, 'uniqueKey')) {
        cur.push({
          index: headerKeys.findIndex(
            item => item === Reflect.get(nxt, 'dataIndex'),
          ),
          dataIndex: Reflect.get(nxt, 'dataIndex') || '',
        });
      }
      return cur;
    },
    [],
  );
  if (colNums?.length > 0) {
    for (let i = 0; i < dataSource.length; i++) {
      const promises: any[] = [];
      for (let j = 0; j < colNums.length; j++) {
        const pic = dataSource[i][colNums[j].dataIndex];
        if (!!pic && picNumber < MAX_EXPORT_PICTURE) {
          promises.push(
            addImage2Cell(
              workbook,
              worksheet,
              pic,
              i + offsetRow,
              colNums[j].index,
            ),
          );
          picNumber++;
        }
      }
      await Promise.all(promises);
    }
  }
}

性能优化

由于cell单元格的大小 = 数据量 * 列数

在数据量和列数同时增长的情况下,这个Excel列表将会无限变大。由于JS是单线程的,如何这么大的计算逻辑和渲染逻辑公用同一个线程的情况下,会直接造成浏览器卡死情况

web Worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

我们将数据获取+数据处理这些消耗CPU的逻辑全部转移到子线程中执行,不干扰主线程的渲染执行。

typescript 复制代码
import * as Comlink from 'comlink';
import { saveAs } from 'file-saver';

export const openDownloadXLSXDialog = (url, saveName) => {
  if (typeof url == 'object' && url instanceof Blob) {
    url = URL.createObjectURL(url); // 创建blob地址
  }
  var aLink = document.createElement('a');
  aLink.href = url;
  aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
  var event;
  if (window.MouseEvent) {
    event = new MouseEvent('click');
  } else {
    event = document.createEvent('MouseEvents');
    event.initMouseEvent(
      'click',
      true,
      false,
      window,
      0,
      0,
      0,
      0,
      0,
      false,
      false,
      false,
      false,
      0,
      null,
    );
  }
  aLink.dispatchEvent(event);
};

const str = `
importScripts("https://workorder-dz.oss-cn-zhangjiakou.aliyuncs.com/qy/workorder/default/comlink.js");
importScripts('https://workorder-dz.oss-cn-zhangjiakou.aliyuncs.com/pic/2023-10-18%2005:58:32_exceljs.min.js');
const DEFAULT_COLUMN_WIDTH = 20;
const DEFAULT_ROW_HEIGHT = 20;
function addHeaderStyle(row, attr) {
  const { color, fontSize, horizontal, bold } = attr || {};
  row.height = DEFAULT_ROW_HEIGHT;
  row.eachCell((cell, colNumber) => {
    cell.fill = {
      type: 'pattern',
      pattern: 'solid',
      fgColor: { argb: color },
    };
    cell.font = {
      bold: bold ?? true,
      size: fontSize ?? 11,
      name: '微软雅黑',
    };
    cell.alignment = {
      vertical: 'middle',
      wrapText: true,
      horizontal: horizontal ?? 'left',
    };
  });
}

// 矩阵反转
function transposeMatrix(matrix) {
    const numRows = matrix.length;
    const numCols = matrix[0].length;
    const result = [];
    for (let col = 0; col < numCols; col++) {
        const newRow = [];
        for (let row = 0; row < numRows; row++) {
            newRow.push(matrix[row][col]);
        }
        result.push(newRow);
    }
    return result;
}

function lastIndexOfObject(array, searchObject, fromIndex = array.length - 1) {
  for (let i = fromIndex; i >= 0; i--) {
      if (array[i] && typeof array[i] === 'object') {
          const keysA = Object.keys(array[i]);
          const keysB = Object.keys(searchObject);
          if (keysA.length === keysB.length &&
              keysA.every(key => array[i][key] === searchObject[key])) {
              return i;
          }
      }
  }
  return -1;
}

function getExcelAlpha(index) {
  let alpha = '';
  let remainder;

  while (index > 0) {
    remainder = (index - 1) % 26;
    alpha = String.fromCharCode(65 + remainder) + alpha;
    index = Math.floor((index - 1) / 26);
  }

  return alpha;
}

const mergeColumnsCell = (
  rowData,
  worksheet,
  rowNumber,
  rowInstance,
) => {
  // 当前列指针
  let pointer = -1;
  rowData.forEach((cellData, index) => {
    if (index <= pointer) return;
    pointer = lastIndexOfObject(rowData, cellData);
    if (index !== pointer) {
      const START = getExcelAlpha(index + 1);
      const END = getExcelAlpha(pointer + 1);
      const position = START + '' + (Number(rowNumber) + 1) + ':' + END + (Number(rowNumber) + 1)
      worksheet.mergeCells(position);
      const cell = rowInstance.getCell(index + 1);
      cell.alignment = {
        vertical: 'middle',
        horizontal: 'center',
        wrapText: true,
      };
    }
  });
};

const mergeRowCells = (colData, worksheet, colNumber) => {
  // 当前列指针
  let pointer = -1;
  colData.forEach((cellData, index) => {
      if (index <= pointer)
          return;
      pointer = lastIndexOfObject(colData, cellData);
      const COL = getExcelAlpha(colNumber + 1);
      if (index !== pointer) {
        const position = COL + '' + (Number(index) + 1) + ':' + COL + (Number(pointer) + 1);
        worksheet.mergeCells(position);
      }
  });
};

function drawExcelHeader(worksheet, headerRows) {
  // 行处理
  headerRows === null || headerRows === void 0 ? void 0 : headerRows.map((row, index) => {
      // 获取单行列头所有名称
      const columnsName = row === null || row === void 0 ? void 0 : row.map(columns => columns.title);
      // 单行列头插入
      const rowHeader = worksheet.addRow(columnsName);
      // 样式修改
      addHeaderStyle(rowHeader, { color: 'dff8ff' });
      // 行合并列头
      mergeColumnsCell(row, worksheet, index, rowHeader);
  });
  if ((headerRows === null || headerRows === void 0 ? void 0 : headerRows.length) > 1) {
      // 二维数组反转
      const transList = transposeMatrix(headerRows);
      transList === null || transList === void 0 ? void 0 : transList.map((row, index) => {
          // 列处理
          mergeRowCells(row, worksheet, index);
      });
  }
}

// 计算组件所有叶子节点个数
const getMaxChildLength = node => {
    if (!node.children || node.children.length === 0) {
        return 1; // 当前节点是叶子节点
    }
    let count = 0;
    for (const childNode of node.children) {
        count += getMaxChildLength(childNode);
    }
    return count;
};

const loopPushCell = (column, rowArray = [[]], index = 0, maxDepth = 0) => {
  var _a, _b, _c;
  if (index >= maxDepth)
      return;
  const count = getMaxChildLength(column);
  const columns = (_b = (_a = new Array(count)) === null || _a === void 0 ? void 0 : _a.fill(null)) === null || _b === void 0 ? void 0 : _b.map(() => ({
      title: column.title,
      dataIndex: column.dataIndex,
  }));
  rowArray[index].push(...columns);
  // 有子节点的时候,继续
  if (Reflect.has(column, 'children')) {
      ++index;
      (_c = column.children) === null || _c === void 0 ? void 0 : _c.map(item => loopPushCell(item, rowArray, index, maxDepth));
  }
  else {
      // 无子节点的时候,子节点的数据继承父节点数据
      loopPushCell(column, rowArray, ++index, maxDepth);
  }
};

// 计算当前列头最大深度
function calculateMaxDepth(columns) {
  let maxDepth = 1;
  function calculateDepth(column, depth) {
      if (column.children) {
          for (const child of column.children) {
              calculateDepth(child, depth + 1);
          }
          maxDepth = Math.max(maxDepth, depth + 1);
      }
  }
  for (const column of columns) {
      calculateDepth(column, 1);
  }
  return maxDepth;
}
function generateHeaders(columns, parentIndex) {
  return columns === null || columns === void 0 ? void 0 : columns.map(col => {
      const obj = {
          // 显示的 name
          header: col.title,
          // 用于数据匹配的 key
          key: col.dataIndex,
          // 列宽
          width: DEFAULT_COLUMN_WIDTH,
      };
      if (parentIndex) {
          obj.parentKey = parentIndex;
      }
      if (Array.isArray(col.children)) {
          obj.children = generateHeaders(col.children, col.dataIndex);
      }
      return obj;
  });
}

function getColumnNumber(width) {
  // 需要的列数,四舍五入
  return Math.round(width / DEFAULT_COLUMN_WIDTH);
}

function mergeRowCell(headers, row, worksheet) {
  // 当前列的索引
  let colIndex = 1;
  headers.forEach(header => {
      const { width, children } = header;
      if (children) {
          children.forEach(child => {
              colIndex += 1;
          });
      }
      else {
          // 需要的列数,四舍五入
          const colNum = getColumnNumber(width);
          // 如果 colNum > 1 说明需要合并
          if (colNum > 1) {
              worksheet.mergeCells(Number(row.number), colIndex, Number(row.number), colIndex + colNum - 1);
          }
          colIndex += colNum;
      }
  });
}

function addData2Table(dataSource, worksheet, headerKeys, headers) {
  dataSource === null || dataSource === void 0 ? void 0 : dataSource.forEach((item) => {
      const rowData = headerKeys === null || headerKeys === void 0 ? void 0 : headerKeys.map(key => item[key]);
      const row = worksheet.addRow(rowData);
      if (Reflect.has(item, 'exportIndex')) {
          if (Number(item === null || item === void 0 ? void 0 : item.exportIndex) % 2 === 0) {
              addHeaderStyle(row, { color: 'e8f3ff' });
          }
      }
      mergeRowCell(headers, row, worksheet);
      row.height = 26;
      // 设置行样式, wrapText: 自动换行
      row.alignment = { vertical: 'middle', wrapText: true, shrinkToFit: false };
      row.font = { size: 11, name: '微软雅黑' };
  });
}

const getWorkBook = ({ columns: fields, dataSource, fileName }) => {
  
  const workbook = new ExcelJS.Workbook();
  const worksheet = workbook.addWorksheet(fileName);
  // 设置 sheet 的默认行高
  worksheet.properties.defaultRowHeight = 20;
  const headers = generateHeaders(fields);
  
  // 计算当前Excel列头最大深度
  const maxDepth = calculateMaxDepth(fields);
  const excelHeaderRow = new Array(maxDepth).fill(null).map(() => []);
  fields?.map(item => loopPushCell(item, excelHeaderRow, 0, maxDepth));
  
  drawExcelHeader(worksheet, excelHeaderRow);
  addData2Table(
    dataSource,
    worksheet,
    excelHeaderRow?.[excelHeaderRow?.length - 1]?.map(
      (item) => item?.dataIndex,
    ),
    headers,
  );
  console.log('xxx1', worksheet.columns, DEFAULT_COLUMN_WIDTH)
  // 给每列设置固定宽度
  worksheet.columns = worksheet.columns.map(col => ({
    ...col,
    width: DEFAULT_COLUMN_WIDTH,
  }));
  return workbook.xlsx.writeBuffer()
};
Comlink.expose(getWorkBook);
`;
const blob = new Blob([str]);
const url = window.URL.createObjectURL(blob);
const worker = new Worker(url);
export const exportExcel = Comlink.wrap(worker);

type exportExcelDefaultType = {
  columns: any[];
  dataSource?: any[];
  fileName: string;
};
// 默认导出表格中的所有数据
export const exportExcelDefault = async ({
  columns = [],
  dataSource = [],
  fileName,
}: exportExcelDefaultType) => {
  // @ts-ignore
  const blobExcel = await exportExcel({
    columns,
    dataSource,
    fileName,
  });
  const blob = new Blob([blobExcel], { type: '' });
  saveAs(blob, `${fileName}.xlsx`);
};

web Worker里面没有办法操作window和document对象,所以针对需要生成图片的场景,没有办法移入到web worker中执行,需要取舍。所以这里在不导出图片的时候走子线程下载。在导出的时候可以让用户自己抉择是否需要下载图片

相关推荐
破z晓21 分钟前
javascript 导出excel表
开发语言·javascript·excel
Am-Chestnuts5 小时前
豆包多轮回答中的表格导出 Excel:编号、日期与长数字检查
excel
hPw0eKIqD18 小时前
使用二次封装的Excel COM 组件操作Excel\WPS ET中的区域、行和列
excel·wps
冰心孤城1 天前
Excel: xls与xlsx格式转换排坑指南
java·前端·excel
金豆呀1 天前
WPS批量提取Word文档内容生成固定格式Excel表格
word·excel·wps
Access开发易登软件1 天前
Access 怎么做前后端分离?用 Web API 读写 SQL Server
前端·数据库·人工智能·microsoft·excel·access
红莲凪是2 天前
使用二次封装的Excel COM 组件操作Excel\WPS ET IExcelRange 高级应用
python·excel·wps
用户298698530142 天前
如何使用免费工具在线将 Excel 转换为 XML 格式
人工智能·后端·excel
开开心心就好2 天前
内存清理工具定时自动清理开机自启动
java·开发语言·elasticsearch·ocr·excel·音视频·big data
开开心心就好2 天前
电脑内存优化工具一键释放内存超简单
java·开发语言·macos·arcgis·ocr·excel·音视频