纯前端倒出excel,大数据导出

2,925 阅读5分钟

需求

需求:客户数据量特别大,每天都有2w条以上的数据。 1、导出10w行*200列以上的数据 2、需要带样式导出 3、支持office 2007以上打开

尝试过的方案

方案一、excle.js

优点:不会有什么兼容性问题
缺点:3000*200以内的数据没有问题;超过就会出现性能问题,性能差的电脑就会出现浏览器崩溃
性能原因:形成xlsx文件的写入方法问题,需要使用 拆分循环 修改形成文件源码,跪求大佬修改一下给小弟学习。

方案二、xlxs.js + xlsx-js-style 、SheetJs等类似xlxs的js库

优点:不会有什么兼容性问题
缺点:
1、文件会比正常的文件会大3倍左右,每一个单元格大概多了32个字符串,
2、跟方案一一样都会出现性能问题
性能原因:形成xlsx文件的写入方法问题,需要使用 拆分循环 修改形成文件源码,跪求大佬修改一下给小弟学习。

方案三、原生table导出

优点:速度非常快,10w*200列的数据,1分钟内导出。使用 拆分循 环封装一下封装数据大渲染模式,就可以解决性能问题。
缺点:
1、office2007以上打开会有告警,但是数据内容是正常的
2、苹果手机 微信打开 文件空白
3、下载的文件,本地编辑插入不了图片

最终方案

使用方案三,客户可以接受其的三个兼容性问题

关键点代码

#### 陈年老项目,不能写ES6,那就将就的使用吧
// 表格数据配置
var params = {
    header: [{a:'表头a',b:'表头b'}], // 一级表头
    headers: [{columnname:'表头a',display:'a',__colSpan:2,columns:[{columnname:'表头a1',display:'a1'},{columnname:'表头a2',display:'a2'}]},{columnname:'表头b',display:'b',columns:[{columnname:'表头b1',display:'b1'}]}], // 多级表头 
    data: [{a:'1',b:'2'}], // 表格数据 []
    statisticsColumns: [], // 合计列数据
    column: [{columnname:'表头a',display:'a',columns:[{columnname:'表头a1',display:'a1'}]},{columnname:'表头b',display:'b',columns:[{columnname:'表头b1',display:'b1'}]}], // 表格列的数据描述
    fileName: 'hhhhhh.xls',  // 导出文件名
    caption: '顶部合并数据', // 表头上的标题
    isShowZero: true, // 是显示 0
    newFn: showExportTip,  // 回调
    customColumnsSHowZero: true  // 兼容以0开头的数据要显示0
}
// 调用
    exportExcelFromFront(params, function (res) {
            showExportTip(res)
    })
        
// 进度监控 用于loding
function showExportTip(type) {
    if (type == 'start') {
    console.log('----导出开始',new Date());
    } else if (type && type != 'start') {
        console.log('----导出进度---',type);
    } else {
    console.log('----导出结束---',type);
    }
}
var newColumnList = {}  // 用来存储每一列数据类型
var newColumnWidthList = {}  // 用来存储每一列 表格宽度
// 初始化 数据 构建数据
function exportExcelFromFront(params, newFn) {
    newColumnList = {}
    newColumnWidthList = {}
    var _params = params,
    cellList = Object.keys(_params.header[0]) || [],
    cellListName = Object.values(_params.header[0]) || [],
    columnList = _params.column || [],
    exportData = _params.data,
    caption = _params.caption || '',
    exportName = _params.fileName || '',
    isShowZero = _params.isShowZero
    headers = _params.headers || []
    var captionEle = '' // 表格标题
    var _headerList = []
    var headerEle = ''
    var widthNum = 1.2 // 列宽倍数 兼容宽度过小问题
    // 自行根据自己的表头 列数据做封装
    if (headers.length > 0) {//  多级表头
        var headerOne = []
        var headerTow = []
        headers.map(function (item) {
            if (item.columns) { // 多级表头 业务最多两级,需要多的自己动手 根据 __colSpan 来合并多少行 colspan表示合并多少列
                item.__colSpan = item.columns.length || item.__colSpan
                headerOne.push(("<th class='h' colspan=" + item.__colSpan + '>').concat(item['display'], '</th>'))
                item.columns.map(function (itemCell) {
                    newColumnList[itemCell.columnname] = itemCell.type
                    var columnWidth = (itemCell.width || itemCell._width || 30) * widthNum
                    headerTow.push(("<th class='h' style='width:" + columnWidth + "px'>").concat(itemCell['display'], '</th>'))
                })
            } else { // 第一级表头
                newColumnList[item.columnname] = item.type
                var columnWidth = (item.width || item._width || 30) * widthNum
                headerOne.push(("<th class='h' rowspan='2' style='width:" + columnWidth + "px'>").concat(item['display'], '</th>'))
            }
        })
        // 表头组装
        headerEle = '<tr>'.concat(headerOne.join(''), '</tr>').concat('<tr>'.concat(headerTow.join(''), '</tr>'))
        // 表头标题组装
        captionEle = caption ? "<th align='left' class='headerTop'" + 'colspan=' + headerOne.length + headerTow.length + '>' + caption + '</th>' : ''
    } else { // 单表头
        columnList.map(function (item) {
            newColumnList[item.columnname] = item.type
            newColumnWidthList[item.display] = (item.width || item._width || 30) * widthNum
        })
        cellListName.map(function (itemCell) {
            _headerList.push("<th class='h' style='width:" + newColumnWidthList[itemCell] + "px'>" + itemCell + '</th>')
        })
        // 表头组装
        headerEle = '<tr>'.concat(_headerList.join(''), '</tr>')
        // 表头标题组装
        captionEle = caption ? "<tr><th align='left' class='headerTop'" + 'colspan=' + _headerList.length + '>' + caption + '</th></tr>' : ''
    }
    var exportDatas = _params.statisticsColumns || {}
    exportDatas[cellList[0]] = '合计: 共' + exportData.length + '行'
    // 运单号以及货号 不为第一项去除 运单号以及货号的合计问题
    if (cellList[0] != 'unit') {
    if (exportDatas && exportDatas['unit']) {
        exportDatas['unit'] = ''
    }
    }
    if (cellList[0] != 'billno') {
    if (exportDatas && exportDatas['billno']) {
        exportDatas['billno'] = ''
    }
    }
    exportData = exportData.concat(exportDatas)
    setTimeout(function () {
        setXlsxData({
            arr: exportData,
            headerList: cellList,
            isShowZero: isShowZero,
            captionEle: captionEle,
            headerEle: headerEle,
            exportName: exportName,
            _cellList: [],
            newFn: newFn,
            customColumnsSHowZero: params.customColumnsSHowZero
        })
    })
}
// 最终导出表格
function newExportWebXlsx(params) {
    if (params.exportName && params.exportName.indexOf('.xls') === -1) {
        params.exportName = params.exportName + new Date().Format('yyyyMMddhhmmss') + '.xls'
    }
    var cellEle = params._cellList.join('')
    var excelContent = ''.concat(params.captionEle).concat(params.headerEle).concat(cellEle)
    var excelFile =
    "<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:x='urn:schemas-microsoft-com:office:excel' xmlns='http://www.w3.org/TR/REC-html40'>"
    excelFile +=
    '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>sheet</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->'
    excelFile +=
    '<style>tr td{text-align:left;height:25px;} .T{vnd.ms-excel.numberformat:yyyy-MM-dd\\ hh\\:mm\\:ss;} .N0{vnd.ms-excel.numberformat:0;text-align:right;} .N1{vnd.ms-excel.numberformat:0.0;text-align:right;} .N2{vnd.ms-excel.numberformat:0.00;text-align:right;} .N3{vnd.ms-excel.numberformat:0.000;text-align:right;} .N4{vnd.ms-excel.numberformat:0.0000;text-align:right;} .S{vnd.ms-excel.numberformat: @;} .headerTop{height:35px;} .h{font-size: 14.7;}</style></head>'
    excelFile += "<body><table border='1' style='font-size: 13.4;font-family: 微软雅黑;'>"
    excelFile += excelContent
    excelFile += '</table></body>'
    excelFile += '</html>'
    var blob = new Blob(['\uFEFF' + excelFile], { type: 'application/vnd.ms-excel;charset=utf-8' })
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
        navigator.msSaveOrOpenBlob(blob, params.exportName)
    } else {
        // 兼容不同浏览器的URL对象
        var url = window.URL || window.webkitURL || window.moxURL
        // 创建下载链接
        var downloadHref = url.createObjectURL(blob)
        // 创建a标签并为其添加属性
        var linka = document.createElement('a')
        linka.href = downloadHref
        linka.style.display = 'none'
        linka.setAttribute('download', params.exportName)
        // 触发点击事件执行下载
        // 火狐浏览器上a标签点击导出无效。解决办法:需要先将a标签添加到当前页面上,在执行click,之后再移除该节点,而不能直接执行click
        document.body.appendChild(linka)
        linka.click()
        // 下载完成进行释放
        url.revokeObjectURL(linka.href)
        document.body.removeChild(linka)
    }
    console.log('----导出结束', new Date())
    params.newFn()
}
// 分批构建表格
function setXlsxData(parms) {
    var s = 1000
    var sum = Math.ceil(parms.arr.length / s)
    var newObj = {}
    for (var i = 0; i < sum; i++) {
        ;(function (j) {
            setTimeout(function () {
            var sIn = j * s
            var endI = sIn + s
            if (j == sum - 1 && parms.arr.length < s * sum) {
                endI = parms.arr.length
            }
            var newArr = parms.arr.slice(sIn, endI)
            newObj[j] = toForXlsxData(newArr, parms.headerList, parms.isShowZero, parms.customColumnsSHowZero)
            checkDataOver(sum, newObj, parms, j)
            }, j * 200)
        })(i)
    }
}
// 检查数据是否组装完成
function checkDataOver(sum, newObj, parms, j) {
    var status = Math.round((j / sum) * 100)
    var statusText = '数据处理进度' + status + '%'
    if (sum === Object.keys(newObj).length) {
        for (var i = 0; i < sum; i++) {
            parms._cellList = parms._cellList.concat(newObj[i])
        }
        parms.newFn('导出文件生成中...')
        setTimeout(function () {
            newExportWebXlsx(parms)
        })
    } else {
        parms.newFn(statusText)
    }
}
// 构建每一个单元格样式
function toForXlsxData(arr, headerList, isShowZero, customColumnsSHowZero) {
    var newArr = []
    // var getNeedWVDataNumber = getNeedWVData()
    for (var i = 0; i < arr.length; i++) {
        var _itemRow = []
        for (var k = 0; k < headerList.length; k++) {
            if (arr[i][headerList[k]]) {
                if (newColumnList[headerList[k]] === 'bigint' || newColumnList[headerList[k]] === 'string') {
                    _itemRow[k] = "<td class='S'>" + arr[i][headerList[k]] + '</td>'
                } else if (newColumnList[headerList[k]] === 'float') {
                    var classNum = toFloatNumCss(arr[i][headerList[k]])
                    _itemRow[k] = "<td class='" + classNum + "'>" + arr[i][headerList[k]] + '</td>'
                } else if (newColumnList[headerList[k]] === 'date') {
                    _itemRow[k] = "<td class='T'>" + arr[i][headerList[k]] + '</td>'
                } else {
                    _itemRow[k] = '<td>' + arr[i][headerList[k]] + '</td>'
                }
            } else {
                // 是否显示0
                _itemRow[k] = '<td>' + (isShowZero ? 0 : '') + '</td>'
            }
            // 处理以0开头的数据
            if (arr[i][headerList[k]] && String(arr[i][headerList[k]]).startsWith('0') && String(arr[i][headerList[k]]).indexOf('.') == -1) {
                _itemRow[k] = "<td class='S'>" + arr[i][headerList[k]] + '</td>'
            }
        }
        newArr.push('<tr>'.concat(_itemRow.join(''), '</tr>'))
    }
    return newArr
}
// 特殊数据类型做处理
function toFloatNumCss(params) {
    var numCss = 'N0'
    var number = params
    if (String(number) && String(number).indexOf('.') == -1) {
        // 非小数的,直接返回
        return numCss
    }
    if (String(number).indexOf('.') > -1 && String(number)) {
        // 存在小数点
        number = parseFloat(params)
        var x = String(number).indexOf('.') + 1 //小数点的位置
        var y = String(number).length - x //小数的位数
        y = y > 4 ? 4 : y
        switch (y) {
            case 1:
            numCss = 'N1'
            break
            case 2:
            numCss = 'N2'
            break
            case 3:
            numCss = 'N3'
            break
            default:
            numCss = 'N4'
            break
        }
    }
    return numCss
}

gitee地址

欢迎下载 gitee.com/sise116/exc…