需求
需求:客户数据量特别大,每天都有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
}