前阵子居家办公接到一个需要前端导出Excel,我兴高采烈的的和产品说这个货色没什么问题,接着产品就具体的把他的需要一字不差的和我讲了遍,过后我想发出我方才说过的话。

大抵需要如下:

页面中须要依据不同的查问条件展现不同的表头信息和表格数据,导出Excel的时候也须要依据以后表头信息导出数据。表头可能是一级,可能是二级,可能是...N级。我过后想拿大刀砍产品。

我和后端磋商了一下这个货色可不可以他们做,为了性能最初决定前后端都得做(谁也跑不了...),数据量大的时候后端导出,数据量小的时候前端导出。

背景故事大略就是这样,因为笔者原来做过Excel导出,然而也只是简略利用而是,波及到二级表头都很少,更别说N级了。为了可能实现这个需要我从新看了一下原来应用的框架以及其余方向的调研,于是通过笔者不懈的致力,繁忙了两个小时的笔者,最初感觉关上淘宝...错了,重来。最终实现了两版,呈现的成果是一样的,然而两者仍存在差异。

导出Excel多级表头 1.0

最开始调研的时候应用的是xlsx这个工具包,因为这个包相对来说比拟是相熟,上手比拟快。过后在最开始要优先思考。

装置依赖:

npm install xlsx -Soryarn add xlsx -S

创立文件DownExcel.js在文件中引入xlsx,废话不多说创立一个class,程序设计后期思考是这样的,在class初始化的时候,须要接管一个tableHead即须要导出数据的表头,下载文件的时候须要调用down办法,在down办法中须要接管data和须要导出的文件名。

class DownExcel {  constructor({ header = [] }) {    this.tableHeader = header;  }    down(fileName, tableData = []){        }    }

最辣手其实不是表格数据的填写,而是如何让导出的Excel反对N级动静表头。因为xlsx框架在导出excel的时候反对Csv模式的,最初将Csv转换成Sheet之后再导出文件,那么也就是说我须要在整顿Csv数据之前,就应该晓得了表头的合并信息。然而xlsx框架的合并信息是独自存储的,大略内容如下:

const marge = [{    s: { r: 0, c: 1 },    e: { r: 0, c: 2 }}];

上述内容中,s代表开始的节点地位,e代表的是完结的节点地位,r代表的是行,c代表的是列。通过剖析数据能够看出。别离确认了两个坐标点,按照两个坐标点进行单元格合并。那么如果想要反对多级表头数据那么就须要反对marge信息是什么,如果能够依据表头信息来生成岂不就行了吗?

页面表格中是存有表头信息的,导出Excel须要依据页面表头来生成一样的。表头数据信息大略是这个样子的:

const tableHeader = [{  field: "a111",  title: "a1"},{  field: "a333",  title: "a3",  children: [{    field: "b111",    title: "b1",  },{    field: "b222",    title: "b2",    children: [{      field: "c1",      title: "c111",      children: [{        file: "d444",        title: "d4"      }]    }]  }]}];

以上就是表头的数据为嵌套数据格式,只有是上级存在children那么该级就须要进行合并,如果没有的话就须要合并到表头的底部。

创立办法resetMergeHeaderInfo

class DownExcel {  down(fileName, tableData = []){    this.resetMergeHeaderInfo();  } // 表头数据                tableHeader // 表头深度                maxLevel // 合并表头长期存储信息    outMarge // 最终后果                result  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){        }    }

在办法中其中有一个参数是maxLevel,这个参数是以后表头信息的最大深度?也就是以后表头信息一共嵌套了多少层。有了这个参数,就能晓得最外层的数据,如果没有children的时候单元格的合并范畴。那么也就须要确认哪些是最外层,哪些是内层的,哪些是有children,哪些是没有children。大抵上在表头中呈现的状况也就这几种了。

首先确认外层数据,如果想实现嵌套数据,比不少会用到递归,须要对外层书进行标记。

class DownExcel { // 表头数据                tableHeader // 表头深度                maxLevel // 合并表头长期存储信息    outMarge // 最终后果                result  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();  }        //  标记最外层数据  tagHeadIn(){    const { tableHeader } = this;    tableHeader.forEach((el) => {      el.isOut = true;      return el;    })  }}

先解决外层数据,方才也说过了,须要晓得最大的嵌套层级是多大,所以这里须要办法获取到所须要的这个参数。

class DownExcel {  down(){    const { tableHeader } = this;    let maxLevel = this.maxLevel(tableHeader);    this.resetMergeHeaderInfo(tableHeader,maxLevel);  }    resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();  }     tagMaxLevel(tableHeader){    const maxLevel = this.maxLevel(tableHeader, false);    tableHeader.forEach((el) => {      if(!el.children){        el.maxLen = maxLevel;      }      else{        this.tagMaxLevel(el.children);        el.maxLen = maxLevel;      }    });  }    }

在标记层级的的时候,在每一层都标注了,他上面的最大层级,这样就不须要每次都须要遍历去去获取最大深度。认为子级也是有嵌套,如果没有嵌套越须要晓得以后单元格向下合并几个,否则会导致合并不能对立。

接下来就是须要解决最外层的表格信息了:

class DownExcel {  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();    for(let i = 0; i < tableHeader.length; i++){        let item = tableHeader[i];         //  纵向跨度         const { maxLen } = item;        //  开始节点信息        let s = {};        //  完结节点信息        let e = {};        //  如果没有子级        if(!item.children){            //  如果是最外层元素            if(item.isOut){                //  当前列开始地位                outMarge.startCell += 1;                //  全局列开始地位                outMarge.basisCell += 1;                //  开始行                s.r = 0;                //  完结行                e.r = maxLevel;                //  开始列                s.c = outMarge.startCell;                //  完结列                e.c = outMarge.startCell;                result.push({ s, e, item });            }else{  //  不是外层元素                            }        }        //  如果有子级        if(item.children){            //  如果是最外层元素            if(item.isOut){                            }else{  //  不是外层元素                            }        }    }  }}

依据已知的信息对外层没有子级的单元格信息曾经获取到了,为什么要记录两个列开始信息?如果A单元格上面有两个子级,一个没有children(A1)另一个有children(A2)确定完信息之后,开始列就会发生变化自增1A2渲染的时候子级有多少个须要在全局记录的行信息上加上,这样进入到B单元格循环的时候才会从对应的中央开始进行下一次合并信息记录。

当有子级的时候就会有一个问题,那么就是以后单元格的横向合并多少个单元格?那么就能够依据以后单元格的children所有没有children的子级,就是以后单元格的横向跨度。

class DownExcel {  //  获取以后上面所有子级  //  即:表头横向跨度单元格数量  getLastChild (arr, result = []){    for(let i = 0,item; item = arr[i++];){      if(!item.children){        result.push(item);      }else{        result = this.getLastChild(item.children, result);      }    }    return result;  }}

晓得了横向跨度之后能够解决内部有子级的单元格合并信息的收集:

class DownExcel {  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();    for(let i = 0; i < tableHeader.length; i++){        let item = tableHeader[i];         //  纵向跨度         const { maxLen } = item;         //  横向跨度        let lastChild = this.getLastChild(item.children || []);        //  开始节点信息        let s = {};        //  完结节点信息        let e = {};        //  如果没有子级        if(!item.children){            //  如果是最外层元素            if(item.isOut){                //  .....            }else{  //  不是外层元素                            }        }        //  如果有子级        if(item.children){            //  如果是最外层元素            if(item.isOut){              // 开始行              s.r = 0;              // 完结行              e.r = 0;              // 部分开始列自增              outMarge.startCell += 1;              // 开始列              s.c = outMarge.startCell;              // 开始列加上横向跨度              outMarge.startCell += lastChild.length - 1;              // 完结列              e.c = outMarge.startCell;              result.push({ s, e, item });            }else{  //  不是外层元素                            }        }    }  }}

因为横向开始地位是须要记录的,所以要加上以后的横向跨度,防止下次循环的开始地位呈现谬误。

class DownExcel {  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();    for(let i = 0; i < tableHeader.length; i++){        let item = tableHeader[i];         //  纵向跨度         const { maxLen } = item;         //  横向跨度        let lastChild = this.getLastChild(item.children || []);        //  开始节点信息        let s = {};        //  完结节点信息        let e = {};        //  如果没有子级        if(!item.children){            //  如果是最外层元素            if(item.isOut){                //  .....            }else{  //  不是外层元素              // 开始行               let r = maxLevel - (outMarge.basisRow + maxLen);              r = Math.max(r, 0);              s.c = outMarge.basisCell;              e.c = outMarge.basisCell;              s.r = outMarge.basisRow;              e.r = r + outMarge.basisRow + maxLen;              result.push({ s, e, item });              //  开始行数据 + 1              outMarge.basisCell += 1;            }        }        //  如果有子级        if(item.children){            //  如果是最外层元素            if(item.isOut){              //    ...             }else{  //  不是外层元素                            }        }    }  }}

解决完内部数据就能够解决外部了,解决外部数据其实和解决内部数据是差不多的。在解决子级的时候,如果单元格上面两个如果A单元格上面有两个子级,一个没有children(A1)另一个有children(A2),这个时候A1须要向下合并到最底部,用最大深度 - (开始地位 + 合并高度)失去差值,即以后单元个的完结地位。

class DownExcel {  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();    for(let i = 0; i < tableHeader.length; i++){        let item = tableHeader[i];         //  纵向跨度         const { maxLen } = item;         //  横向跨度        let lastChild = this.getLastChild(item.children || []);        //  开始节点信息        let s = {};        //  完结节点信息        let e = {};        //  如果没有子级        if(!item.children){            //  如果是最外层元素            if(item.isOut){                //  .....            }else{  //  不是外层元素               //   .....            }        }        //  如果有子级        if(item.children){            //  如果是最外层元素            if(item.isOut){              //    ...             }else{  //  不是外层元素                s.c = outMarge.basisCell;                e.c = outMarge.basisCell + lastChild.length - 1;                s.r = outMarge.basisRow;                e.r = outMarge.basisRow;                result.push({ s, e, item });            }        }    }  }}

这里解决逻辑和外层有子级的解决逻辑是相似的。

class DownExcel {  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();    for(let i = 0; i < tableHeader.length; i++){        let item = tableHeader[i];         //  纵向跨度         const { maxLen } = item;         //  横向跨度        let lastChild = this.getLastChild(item.children || []);        //  开始节点信息        let s = {};        //  完结节点信息        let e = {};        //  如果没有子级        if(!item.children){            //  如果是最外层元素            if(item.isOut){                //  .....            }else{  //  不是外层元素               //   .....            }        }        //  如果有子级        if(item.children){            //  如果是最外层元素            if(item.isOut){              //    ...             }else{  //  不是外层元素              // ....            }        }        outMarge.basisRow += 1;        this.resetMergeHeaderInfo(item.children, maxLevel, outMarge, result);    }    outMarge.basisRow -= 1;    return result;  }}

解决完收集信息,须要把所有子级的信息收集一下,须要把本地的列数自增一下,保障所和合并的时候合并坐标值是随着遍历和递归是同步进行的。每次递归完须要减一,也就是递归一次须要加一次。

这样针对合并信息收集就实现了,失去的数据和xlsx框架所须要的是统一的。

class DownExcel {  down(fileName, tableData = []){    const { tableHeader, outMarge } = this;    let maxLevel = this.maxLevel(tableHeader);    const mergeInfo = this.resetMergeHeaderInfo(tableHeader, maxLevel, outMarge);    const lastChild = this.getLastChild(tableHeader);    const headCsv = this.getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo);    const dataCsv = this.getDataCsv(tableData, lastChild);    const allCsv = this.margeCsv(headCsv, dataCsv);  }      //  将数据转换成Csv格局  getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo){    let csvArr = [];    let csv = "";    for(let i = 0; i < (maxLevel + 1); i++){      let item = [];      for(let j = 0; j < lastChild.length; j++){        item.push(null);      }      csvArr.push(item);    }    for(let i = 0; i < mergeInfo.length; i++){      let info = mergeInfo[i];      const { s, item } = info;      const { c, r } = s;      const { title } = item;      csvArr[r][c] = title;      console.log(mergeInfo);    }    csvArr = csvArr.map((el) => {      return el.join("^");    });    return csvArr.join("~");  }    //  获取data的Csv  getDataCsv(data, lastChild){    let result = [];    for(let j = 0, ele; ele = data[j++];){      let value = [];      for(let i = 0, item; item = lastChild[i++];){        value.push(ele[item.field] || "-");      };      result.push(value);    }    result = result.map((el) => {      return el.join("^");    });    return result.join("~");  }    //  合并Csv  margeCsv(headCsv, dataCsv){    return `${headCsv}~${dataCsv}`;  }  }

合并信息处理完了之后须要把以后的表头数据和列表数据处理成csv格局,不便导出excel,首先思考的是表头合并信息局部数据须要用空数据代替,不能把所有的数据间接填写进去,否则在导出的时候会产生数据地位错乱的问题。

class DownExcel {  down(fileName, tableData = []){    const { tableHeader, outMarge } = this;    let maxLevel = this.maxLevel(tableHeader);    const mergeInfo = this.resetMergeHeaderInfo(tableHeader, maxLevel, outMarge);    const lastChild = this.getLastChild(tableHeader);    const headCsv = this.getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo);    const dataCsv = this.getDataCsv(tableData, lastChild);    const allCsv = this.margeCsv(headCsv, dataCsv);        const cscSeet = this.csv2sheet(allCsv);    console.log(cscSeet);    let blob = this.sheet2blob(cscSeet);    this.openDownloadDialog(blob,`${fileName}.xlsx`);  }    //  将 csv转换成sheet数据  csv2sheet(csv) {    csv = csv.split('~');    //  缓存    let arr = [];    //  剪切未数组    csv.forEach((el) => {      //  剪切数据并增加答题arr      arr.push(el.split("^"));    });    //  调用办法    return XLSX.utils.aoa_to_sheet(arr);  }    //  sheet转blob文件  sheet2blob(sheet, sheetName) {    //  导出文件类型    sheetName = sheetName || 'sheet1';    var workbook = {      SheetNames: [sheetName],      Sheets: {}    };    workbook.Sheets[sheetName] = sheet;    var wopts = {      bookType: "xlsx",      bookSST: false,      type: 'binary'    };    var wbout = XLSX.write(workbook, wopts);    var blob = new Blob([s2ab(wbout)], {type:"application/octet-stream"});    // 字符串转ArrayBuffer    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;    }    return blob;  }    // 导出excel  openDownloadDialog(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 || '';    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);  }  }

Csv转换成sheet数据,并把合并信息赋值给sheet就能够就能够通过调用框架的办法转换成blob对象实现最初的数据导出。

残缺代码:

import * as XLSX from "xlsx";export default class DownExcel {  constructor({ header = [] }) {    this.tableHeader = header;    this.outMarge = {      startCell: -1,      basisRow: 0,      basisCell: 0,      maxRow: 0    };  }  down(fileName, tableData = []){    const { tableHeader, outMarge } = this;    let maxLevel = this.maxLevel(tableHeader);    const mergeInfo = this.resetMergeHeaderInfo(tableHeader, maxLevel, outMarge);    const lastChild = this.getLastChild(tableHeader);    const headCsv = this.getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo);    const dataCsv = this.getDataCsv(tableData, lastChild);    const allCsv = this.margeCsv(headCsv, dataCsv);    const cscSeet = this.csv2sheet(allCsv);    cscSeet['!merges'] = mergeInfo;    console.log(cscSeet);    let blob = this.sheet2blob(cscSeet);    this.openDownloadDialog(blob,`${fileName}.xlsx`);  }  //  sheet转blob文件  sheet2blob(sheet, sheetName) {    //  导出文件类型    sheetName = sheetName || 'sheet1';    var workbook = {      SheetNames: [sheetName],      Sheets: {}    };    workbook.Sheets[sheetName] = sheet;    var wopts = {      bookType: "xlsx",      bookSST: false,      type: 'binary'    };    var wbout = XLSX.write(workbook, wopts);    var blob = new Blob([s2ab(wbout)], {type:"application/octet-stream"});    // 字符串转ArrayBuffer    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;    }    return blob;  }  //  导出Excel  openDownloadDialog(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 || '';    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);  }  //  获取data的Csv  getDataCsv(data, lastChild){    let result = [];    for(let j = 0, ele; ele = data[j++];){      let value = [];      for(let i = 0, item; item = lastChild[i++];){        value.push(ele[item.field] || "-");      };      result.push(value);    }    result = result.map((el) => {      return el.join("^");    });    return result.join("~");  }  //  将数据转换成Csv格局  getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo){    let csvArr = [];    let csv = "";    for(let i = 0; i < (maxLevel + 1); i++){      let item = [];      for(let j = 0; j < lastChild.length; j++){        item.push(null);      }      csvArr.push(item);    }    for(let i = 0; i < mergeInfo.length; i++){      let info = mergeInfo[i];      const { s, item } = info;      const { c, r } = s;      const { title } = item;      csvArr[r][c] = title;      console.log(mergeInfo);    }    csvArr = csvArr.map((el) => {      return el.join("^");    });    return csvArr.join("~");  }  //  合并Csv  margeCsv(headCsv, dataCsv){    return `${headCsv}~${dataCsv}`;  }  //  将 csv转换成sheet数据  csv2sheet(csv) {    csv = csv.split('~');    //  缓存    let arr = [];    //  剪切未数组    csv.forEach((el) => {      //  剪切数据并增加答题arr      arr.push(el.split("^"));    });    //  调用办法    return XLSX.utils.aoa_to_sheet(arr);  }  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){    this.tagHeadIn();    this.tagMaxLevel(tableHeader);    for(let i = 0; i < tableHeader.length; i++){      let item = tableHeader[i];      //  纵向跨度      const { maxLen } = item;      //  横向跨度      let lastChild = this.getLastChild(item.children || []);      //  s :  开始  e : 完结      //  c : 列(横向)        //  r : 行(纵向)      let s = {};      let e = {};      if(!item.children){        if(item.isOut){          outMarge.startCell += 1;          outMarge.basisCell += 1;          s.r = 0;          e.r = maxLevel;          s.c = outMarge.startCell;          e.c = outMarge.startCell;          result.push({ s, e, item });        }        else{          let r = maxLevel - (outMarge.basisRow + maxLen);          r = Math.max(r, 0);          s.c = outMarge.basisCell;          e.c = outMarge.basisCell;          s.r = outMarge.basisRow;          e.r = r + outMarge.basisRow + maxLen;          result.push({ s, e, item });          outMarge.basisCell += 1;        }      };      if(item.children){        if(item.isOut){          s.r = 0;          e.r = 0;          outMarge.startCell += 1;          s.c = outMarge.startCell;          outMarge.startCell += lastChild.length - 1;          e.c = outMarge.startCell;          result.push({ s, e, item });        }else{          s.c = outMarge.basisCell;          e.c = outMarge.basisCell + lastChild.length - 1;          s.r = outMarge.basisRow;          e.r = outMarge.basisRow;          result.push({ s, e, item });        }        outMarge.basisRow += 1;        this.resetMergeHeaderInfo(item.children, maxLevel, outMarge, result);      };    };    outMarge.basisRow -= 1;    return result;  }  tagHeadIn(){    const { tableHeader } = this;    tableHeader.forEach((el) => {      el.isOut = true;      return el;    })  }  //  标记最大层级  tagMaxLevel(tableHeader){    const maxLevel = this.maxLevel(tableHeader, false);    tableHeader.forEach((el) => {      if(!el.children){        el.maxLen = maxLevel;      }      else{        this.tagMaxLevel(el.children);        el.maxLen = maxLevel;      }    });  }  //  获取最大层级  //  只蕴含子级最大层级(不蕴含本级)  maxLevel(arr, isSetFloor = true){    let floor = -1;    let max = -1;    function each (data, floor) {      data.forEach(e => {        max = Math.max(floor, max);        isSetFloor && (e.floor = (floor + 1));        if (e.children) {          each(e.children, floor + 1)        }      })    }    each(arr,0)    return max;  }  //  获取以后上面所有子级  //  即:表头横向跨度单元格数量  getLastChild (arr, result = []){    for(let i = 0,item; item = arr[i++];){      if(!item.children){        result.push(item);      }else{        result = this.getLastChild(item.children, result);      }    }    return result;  }};

应用:

new DownExcel({  header:  [{    field: "c1",    title: "c111",    children: [{      field: "c2",      title: "c222"    },{      field: "c3",      title: "c333"    },{      field: "c4",      title: "c444"    }]  }]}).down(`${+new Date()}`,[{  a111: "LLL",  c1: "HHH",  c2: "AAA",  a444: "III"}]);

感激大家破费很长时间来浏览这篇文章,若文章中有谬误灰常感激大家提出斧正,我会尽快做出批改的。如果大家喜爱的话接下来会更新一下,导出带入款式并且反对多级表头。