前端JS实现导出Excel 可分页

    技术2024-03-31  66

    很多人都不愿意在前端导出文件,在这里我本人并不完全赞同,如果数据量小并且访问量大的导出功能,也放在后端去实现,只会增加了服务器的压力,当今客户端的处理器也是挺厉害的,何不把服务导出的压力分摊给每个客户端以此减少服务端的压力省掉更多的资源去做更多的事情,给用户的体验岂不是更好。

    百度云盘:

    链接:https://pan.baidu.com/s/1dy8s8SPIy5zxXoqwgN6XcA 提取码:d535

    更新内容:

    版本v1.0.2         1.加入了观察模式 2.添加了异步同步二选一操作 3.删除了依赖jQuery 4.按照ES6语法规范编写,模块化 5.优化了原来的配置复杂,使配置更加清晰,操作更加简便 注意:时间可能稍有误差,具体还要根据自己的电脑配置以及数据格式等影响

    目录结构图:

    Core目录

    Excel.js
    import excelXml from './xml.js' import Utils from '../Utils/Utils.js' import Row from './Row.js' import Table from './Table.js' import UUID from '../Utils/UUID.js' /** * Excel导出工具类,核心类 * @constructor */ class Excel { constructor() { this.excelXml = excelXml; /**用来缓存多个sheet的xml,便于一次导出**/ this.sheetsXml = []; this.excelName = 'gl开源项目'; } /** * 添加caption部门,可以添加多个 * @param headArr * @returns {ExcelUtils} */ addSheet(option) { let tableModel = new Table(); //sheet名字 tableModel.sheetName = option.sheetName; //head信息 tableModel.heads = option.heads; //列名 tableModel.colNames = Object.keys(option.dataMap); //将数据转为数组 tableModel.rows = this.getAxisData(option.dataMap) //将table数据转为html let tableHtml = this.tableModelConvertToTableHtml(tableModel) //将(table)html转为sheet(Html) let sheetXml = this.tableHtmlConvertToSheetHtml(tableModel, tableHtml) this.sheetsXml.push(sheetXml) return this } /** * 处理数据 * @param userPointFunction * @param dataList * @returns {[]} */ getAxisData(dataMap) { const rows = []; if (dataMap != null && dataMap != {}) { //获得列数据最长的长度 let maxLength = 0; Object.keys(dataMap).forEach(colName => { const dataLength = dataMap[colName].length; maxLength = dataLength > maxLength ? dataLength : maxLength; }) //将数据转为数据模型(row) for (let index = 0; index < maxLength; index++) { const row = new Row(); Object.keys(dataMap).forEach(colName => { row[colName] = dataMap[colName][index]; }) rows.push(row); } } return rows; } /** * 将table数据转为html格式的table表格字符串 * version 1.0.1 */ tableModelConvertToTableHtml(tableModel) { const tableId = UUID.getUUID(); tableModel.tableId = tableId; let table = '<table id="' + tableId + '">{tr}</table>' if (!Utils.isEmpty(tableModel.heads)) { tableModel.heads.forEach((elem, index) => { let tr = '<tr>{td}</tr>' let td = '<td colspan=' + elem.colspan + '>' + elem.text + '</td>' tr = Utils.formatStr(tr, td) table = Utils.formatStr(table, tr + '{tr}') }) } let tr = '<tr>{td}</tr>' //添加首行列名字 tableModel.colNames.forEach((elem, index) => { let td = '<td>' + elem + '</td>'; tr = Utils.formatStr(tr, td + '{td}'); }) tr = Utils.formatStr(tr, ''); table = Utils.formatStr(table, tr + '{tr}'); //数据填充table let strL = ''; tableModel.rows.forEach((row, index) => { let tr = '<tr>{td}</tr>'; Object.keys(row).forEach((prop, index) => { let td = '<td>' + row[prop] + '</td>'; tr = Utils.formatStr(tr, td + '{td}'); }) tr = Utils.formatStr(tr, ''); strL += tr; }) table = Utils.formatStr(table, strL); return table; } /** *将table对象转为Sheet * @param sheetName * @param table * @returns {string} */ tableHtmlConvertToSheetHtml(tableModel, tableHtml) { let ctx = ''; let worksheetsXML = ''; let rowsXML = ''; let table = new DOMParser().parseFromString(tableHtml, 'text/html').getElementById(tableModel.tableId); let columnStr = ''; //设置单元格宽度 if (table.rows[tableModel.heads.length].cells.length) { //从非头信息行开始 for (let j = 0; j < table.rows[tableModel.heads.length].cells.length; j++) { columnStr += '<Column ss:Index="' + (j + 1) + '" ss:AutoFitWidth="0" ss:Width="156.75"/>'; } } //控制要导出的行数 for (let j = 0; j < table.rows.length; j++) { //设置行高 if (!Utils.isEmpty(tableModel.heads) && j < tableModel.heads.length) { //头信息高度 rowsXML += '<Row ss:Height="26">'; } else { //非头信息高度 rowsXML += '<Row ss:Height="20">'; } for (let k = 0; k < table.rows[j].cells.length; k++) { let dataValue = table.rows[j].cells[k].innerHTML; let colspan = table.rows[j].cells[k].getAttribute('colspan'); //单元格样式 let styleId = 's35'; //头信息样式 if (!Utils.isEmpty(tableModel.heads) && j < tableModel.heads.length) { styleId = 's32'; } else if (j == tableModel.heads.length) { //非头信息样式 styleId = 's34'; } ctx = { attributeStyleID: styleId != '' ? 'ss:StyleID="' + styleId + '"' : '', nameType: 'String', data: dataValue, mergeAcross: colspan ? ' ss:MergeAcross="' + (colspan - 1) + '"' : '', //合并单元格 }; styleId = ''; rowsXML += Utils.format(this.excelXml.tmplCellXML, ctx); } rowsXML += '</Row>'; } ctx = { rows: rowsXML, nameWS: tableModel.sheetName, column: columnStr } worksheetsXML += Utils.format( this.excelXml.tmplWorksheetXML, ctx ); return worksheetsXML; } //开始导出 run () { let strXml = ''; //拼接所有的sheet xml文档 this.sheetsXml.forEach((sheetXml, index) => { strXml += sheetXml; }) let ctx = { created: new Date().getTime(), worksheets: strXml, } //获得最终的workbookXML let workbookXML = Utils.format( this.excelXml.tmplWorkbookXML, ctx ); this.downExcel(this.excelName, workbookXML); this.clear(); } downExcel(fileName, workbookXML) { let link = document.createElement('A'); link.href = this.excelXml.uri + Utils.base64(workbookXML); link.download = fileName || 'Workbook.xlsx'; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } clear() { this.sheetsXml = []; } } export default Excel;
    Row.js
    /** * 默认的Point模型,用户遍历属性 * @constructor */ class Row {} export default Row;
    Table.js
    /** * table模型 * @constructor */ class Table { constructor() { this.tableId = ''; this.sheetName = ''; this.heads = []; this.colNames = []; this.rows = []; } } export default Table;
    xml.js
    /** * cell,row,sheet,workbook的xml * @type {{tmplCellXML: string, tmplWorksheetXML: string, tmplWorkbookXML: string, uri: string}} * ExpandedColumnCount="256" 每页最多256列 * ss:ExpandedRowCount="10000000" 每页最多1百万行 */ let excelXml = { //uri: 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,', uri: 'data:application/vnd.ms-excel;base64,', tmplWorkbookXML: `<?xml version="1.0"?> <?mso-application progid="Excel.Sheet"?> <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40"> <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office"> <Created>1996-12-17T01:32:42Z</Created> <LastSaved>2006-02-17T13:16:06Z</LastSaved> <Version>11.5606</Version> </DocumentProperties> <OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office"> <RemovePersonalInformation/> </OfficeDocumentSettings> <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel"> <WindowHeight>4530</WindowHeight> <WindowWidth>8505</WindowWidth> <WindowTopX>480</WindowTopX> <WindowTopY>120</WindowTopY> <AcceptLabelsInFormulas/> <ProtectStructure>False</ProtectStructure> <ProtectWindows>False</ProtectWindows> </ExcelWorkbook> <Styles> <Style ss:ID="Default" ss:Name="Normal"> <Alignment ss:Vertical="Bottom"/> <Borders/> <Font ss:FontName="宋体" x:CharSet="134" ss:Size="12"/> <Interior/> <NumberFormat/> <Protection/> </Style> <Style ss:ID="s32"> <Alignment ss:Horizontal="Center" ss:Vertical="Center"/> <Borders>' + <Border ss:Position="Bottom" ss:LineStyle="Dash" ss:Weight="1"/> </Borders>' + <Font ss:FontName="宋体" x:CharSet="134" ss:Size="12" ss:Bold="1"/> <Interior ss:Color="#FFFF00" ss:Pattern="Solid"/> </Style> <Style ss:ID="s34"> <Alignment ss:Horizontal="Center" ss:Vertical="Center"/> <Borders> <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/> <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/> <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/> <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/> </Borders> <Font x:Family="Swiss" ss:Size="11" ss:Bold="1"/> <Interior ss:Color="#CCFFCC" ss:Pattern="Solid"/> </Style> <Style ss:ID="s35"> <Alignment ss:Horizontal="Center" ss:Vertical="Center"/> <Font ss:FontName="宋体" x:CharSet="134" ss:Size="10"/> </Style> </Styles> {worksheets} </Workbook> `, // tmplWorksheetXML: '<Worksheet ss:Name="{nameWS}"><Table ss:ExpandedColumnCount="2" ss:ExpandedRowCount="22"ss:DefaultColumnWidth="156.75" ss:DefaultRowHeight="14.25">{column}{rows}</Table></Worksheet>', tmplWorksheetXML: `<Worksheet ss:Name="{nameWS}"> <Table ss:ExpandedColumnCount="256" ss:ExpandedRowCount="10000000" x:FullColumns="1" x:FullRows="1" ss:DefaultColumnWidth="54" ss:DefaultRowHeight="14.25"> {column}{rows}</Table></Worksheet>`, tmplCellXML: `<Cell {attributeStyleID}{mergeAcross}><Data ss:Type="{nameType}">{data}</Data></Cell>`, }; export default excelXml;

    Observable目录

    观察者模式所需类

    CycleEnum.js
    class Cycle { constructor(name, code) { //constructor是一个构造方法,用来接收参数 this.name = name; //this代表的是实例对象 this.code = code; } } const CycleEnum = { STARTED: new Cycle('STARTED', 1), RUNNING: new Cycle('RUNNING', 2), DONE: new Cycle('DONE', 3), ERROR: new Cycle('ERROR', 4), get values() { return [this.STARTED, this.RUNNING, this.DONE, this.ERROR] }, getNameByCode(code) { this.values.forEach(cycle => { console.log(cycle); }); } }; export default CycleEnum;
    Observable.js
    /*该接口主要是暴漏给调用者使用的,其中四个枚举类型分别代表着当前任务执行生命周期的各个阶段,具体如下: getCycle()方法用于获取当前任务处于哪个执行阶段 start()方法的目的主要是为了屏蔽Thread类其他的API,可同通过Observable的start对线程进行启动。 interrupt()方法的作用和start一样,可通过Observable的interrupt对当前线程进行中断。*/ class Observable { constructor() {} //获取当前任务的生命周期状态 //返回值 Cycle 类型 getCycle() {}; //定义启动任务的方法 //返回值 start() {}; //定义终止任务的方法 interrput() {}; } export default Observable;
    ObservableThread.js
    import Observable from './Observable.js' import TaskLifecycle from './TaskLifecycle.js' import CycleEnum from './CycleEnum.js' import Task from './Task.js' import Thread from '../Thread/Thread.js' class ObservableThread extends Observable { constructor(task, async) { super(); this.constructorTwo(new TaskLifecycle.EmptLifecycle(), task, async); } constructorTwo(taskLifecycle, task, async) { if (task === null) { throw Error('The task is required'); } this.async = async; this.tasklifecycle = taskLifecycle; this.task = new Task.CustomizeTask(task); } execute() { let that = this; const thread = new Thread(() => { that.update(CycleEnum.STARTED, null, null); try { that.update(CycleEnum.RUNNING, null, null); const result = that.task.call(); that.update(CycleEnum.DONE, result, null); } catch (e) { that.update(CycleEnum.ERROR, null, e); } }); this.async === true ? thread.start() : thread.run(); } update(cycle, result, exception) { this.cycle = cycle; if (this.tasklifecycle === null) { return; } try { switch (cycle) { case CycleEnum.STARTED: this.tasklifecycle.onStart(this.thread); break; case CycleEnum.RUNNING: this.tasklifecycle.onRunning(this.thread); break; case CycleEnum.DONE: this.tasklifecycle.onFinsh(this.thread, result); break; case CycleEnum.ERROR: this.tasklifecycle.onError(this.thread, exception); break; } } catch (e) { if (cycle === CycleEnum.ERROR) { throw Error(e.message); } } } } export default ObservableThread;
    Task.js
    /* Task函数接口定义 由于wine需要对线程中的任务执行增加可观察的能力,并且需要获得最后的计算结果,因此Runnable接口在可观察的线程中讲不再使用,取而代之的时Task接口,其作用与Runnable类似,主要英语承载任务的逻辑执行单元。*/ class Task { static CustomizeTask = null; constructor() {} call() {} } class CustomizeTask extends Task { constructor(task) { super() this.task = task; } call() { return this.task.run(); } } Task.CustomizeTask = CustomizeTask; export default Task;
    TaskLifecycle.js
    import versionInfo from '../Version/Version.js' //TaskLifecycle接口定义了在任务执行的生命周期总会被触发的接口, 其中EmptyLifecycle是一个空实现( 在这里我加了实现), 主要是为了让使用者保持对Thread类的使用习惯。 // //onStart(Thread thread) 当任务开始执行时会被回调的方法。 //onRunning(Thread thread) 任务运行时被回调的方法, 由于我们针对的时任务的生命周期, 不同于线程生命周期中的RUNNING状态, 如果当前线程进入休眠或者阻塞那么任务都是running状态。 //onFinish(Thread thread, T result) 任务正确执行结束后会被回调, 其中result时任务执行后的结果, 可允许为null。 //onError(Thread thread, Exception e) 任务在运行过程中出现任何异常抛出时, onError方法都将被回调, 并将异常信息一并传入。 class TaskLifecycle { static EmptLifecycle = null; constructor() { } //任务启动触发方法 onStart(thread) {}; //任务进行时触发方法 onRunning(thread) {}; //任务完成时触发方法 onFinish(thread, result) {}; //任务执行报错时触发方法 onError(thread, exception) {}; } //实现子类 class EmptLifecycle extends TaskLifecycle { constructor() { super(); } onStart(thread) { console.log(versionInfo.version) console.log(versionInfo.author) console.log(versionInfo.other) console.log('开始任务'); } onRunning(thread) { console.log('正在执行'); } onFinsh(thread, result) { console.log('任务执行完毕,结果是' + result); } onError(thread, exception) { console.log('任务执行出现异常', exception); } } TaskLifecycle.EmptLifecycle = EmptLifecycle; export default TaskLifecycle;

    Tread目录

    Thread.js
    import UUID from '../Utils/UUID.js' class Runnable { constructor(fn) { this.run = fn } } class Thread extends Runnable { constructor(task) { super(); this.task = new Runnable(task); this.sleep = 0; this.name = 'Thread-' + UUID.getUUID(); this.interruptFlat = false; } //同步执行 run() { try { console.log(this.task.run()) this.task.run(); } catch (e) { this.uncaughtExceptionHanlder(e); } } //异步执行 start() { setTimeout(this.task.run, this.sleep); } uncaughtExceptionHanlder(e) { console.log(e.message); } //中断方法 interrupt() { this.interruptFlat = true } //作用是测试当前线程是否被中断(检查 中断标志位),返回一个boolean并清除中断状态,第二次再调用中断状态已经被清除,将返回false interrupted() { let tempFlat = this.interruptFlat; this.interruptFlat = false; return tempFlat; } //作用是只测试此线程是否被中断,不清除中断状态 isInterrupted() { return this.interruptFlat; } setSleep(millisecond) { this.sleep = millisecond; } } export default Thread;

    Utils目录

    Utils.js
    class Utils { /** * 判断字符和数组 * @param data * @returns {boolean} */ static isEmpty = function (data) { if (data === undefined) { return true; } var type = typeof data; if (type === 'object') { if (typeof data.length === 'number') { type = 'array'; } else { type = 'object'; } } switch (type) { case 'array': if (data === undefined || data.length <= 0) { return true; } else { return false; } case 'string': if (data === undefined || data.length() <= 0) { return true; } else { return false; } default: throw new Error('Unknown type'); } } /** * @param sheetName * @param table * @returns {string} */ static formatStr = function (s, c) { return s.replace(/{(\w+)}/g, (m, p) => { return c; }); } /** * {xxx}数据替换 * @param s * @param c * @returns {*|void|string} */ static format = function (s, c) { return s.replace(/{(\w+)}/g, (m, p) => { return c[p]; }); } /** * 将workbookxml转为base64数据 * @param s * @returns {string} */ static base64 = s => { let str = window.btoa(unescape(encodeURIComponent(s))); return str; } } export default Utils;
    UUID.js
    class UUID { constructor() {} static S4() { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); } static getUUID() { return (this.S4() + this.S4() + "-" + this.S4() + "-" + this.S4() + "-" + this.S4() + "-" + this.S4() + this.S4() + this.S4()); } } export default UUID;

    Version目录

    Version.js
    const author = '作者: guolei'; const version = '版本: v1.0.2'; const other = '作品: 完全免费'; export default { author, version, other };

    GL_Excel.js

    import ObservableThread from './Observable/ObservableThread.js' import Excel from './Core/Excel.js' class GL_Excel { constructor() { this.excel = new Excel(); } setExcelName(name) { this.excel.excelName = name; } setSheetOption(option) { if (this.excel.excelName != null && this.excel.excelName != '') { this.excel.addSheet(option); return this; } else { throw Error('请先设置excel名称'); } } export (async) { let observable = new ObservableThread(this.excel, async); observable.execute(); } } export default GL_Excel;

    test目录 测试使用 test.html

    <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8"> <title></title> <script type="module"> import GL_Excel from '../GL_Excel.js' (function(doc, Excel){ const exportBtn = doc.querySelector('#export'); const input = doc.querySelector('#count'); const init = () =>{ bindEvent(); }; function bindEvent(){ exportBtn.addEventListener('click', btnClickHandle, false); } function btnClickHandle(){ const count = input.value; let excel = new Excel(); excel.setExcelName('gl开源'); //添加第一页 excel.setSheetOption({ sheetName: '第一页', heads: [{ colspan: 3, text: '电力' }], dataMap: { '电压': [...new Array(count*10000).keys()], '电流': [...new Array(count*10000).keys()], '电阻': [...new Array(count*10000).keys()] } }) // //添加第二页 // excel.setSheetOption({ // sheetName: '第二页', // heads: [{ // colspan: 3, // text: '风力' // }], // dataMap: { // '方向': [1,2,3,4].reverse(), // '大小': [1,2,3,4].reverse(), // '阻力': [1,2,3,4].reverse() // } // }) //导出 excel.export(true); } init(); }(document, GL_Excel)) </script> </head> <body> <label for="count">输入导出的记录数</label> <input id="count" />万 <button id="export">导出</button> </body>         </html>   

    test页面启动后 导出效果: 文件效果: 多页: test页中去掉以下代码的注释

    //添加第二页 excel.setSheetOption({ sheetName: '第二页', heads: [{ colspan: 3, text: '风力' }], dataMap: { '方向': [1,2,3,4].reverse(), '大小': [1,2,3,4].reverse(), '阻力': [1,2,3,4].reverse() } })

    如有缺点请留下宝贵的意见,不定时更新版本。

    Processed: 0.048, SQL: 12