使用js-xlsx純前端導出excel

前言

最近公司需要將幾張統計表格導出到excel,由於公司現有導出excel功能是前後端配合的導出,覺得麻煩,所以想找一個純前端導出的工具,最後找到了js-xlsx,評價還是挺高的,但是中文文檔沒找到,百度也沒有找到一個比較全面的教程,所以踩了很多坑,自己記錄下,方便以後使用。

環境

由於我業務只用到將table標籤內的內容導出到excel,所以只會寫如何將一個table元素裏的內容導出到excel。也可以通過json導出,貌似還會更簡單些。

安裝

GitHub地址
npm安裝

npm install xlsx

安裝後dist文件夾下有一個文件xlsx.full.min.js,就是它了,引入到項目中

第一個例子

先上代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <table id="table1" border="1" cellspacing="0" cellpadding="0" >
        <thead>
            <tr>
                <td>序號</td>
                <td>姓名</td>
                <td>年齡</td>
                <td>興趣</td>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>張三</td>
                <td>18</td>
                <td>打遊戲</td>
            </tr>
            <tr>
                <td>2</td>
                <td>李四</td>
                <td>88</td>
                <td>看電影</td>
            </tr>
            <tr>
                <td>3</td>
                <td>王五</td>
                <td>81</td>
                <td>睡覺</td>
            </tr>
        </tbody>
    </table>

    <button id="btn" onclick="btn_export()">導出</button>
</body>
<script src="js/xlsx.full.min.js"></script>
<script src="js/export.js"></script>
<script>
    function btn_export() {
        var table1 = document.querySelector("#table1");
        var sheet = XLSX.utils.table_to_sheet(table1);//將一個table對象轉換成一個sheet對象
        openDownloadDialog(sheet2blob(sheet),'下載.xlsx');
    }
</script>
</html>

運行效果
這是個運行效果

導出結果:

在這裏插入圖片描述
你可能注意到了,我這裏引入了一個export.js文件,這個export.js文件裏面只有2個方法,就是上面代碼用到的openDownloadDialog(sheet2blob(sheet),‘下載.xlsx’);
這是export.js的代碼:

// 將一個sheet轉成最終的excel文件的blob對象,然後利用URL.createObjectURL下載
function sheet2blob(sheet, sheetName) {
    sheetName = sheetName || 'sheet1';
    var workbook = {
        SheetNames: [sheetName],
        Sheets: {}
    };
    workbook.Sheets[sheetName] = sheet; // 生成excel的配置項

    var wopts = {
        bookType: 'xlsx', // 要生成的文件類型
        bookSST: false, // 是否生成Shared String Table,官方解釋是,如果開啓生成速度會下降,但在低版本IOS設備上有更好的兼容性
        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;
}

function 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 || ''; // 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);
}

PS: 這2個方法是網上當的,原文地址。作者寫的挺好,也是從這裏找到了頭緒。

如果你的table標籤內有合併單元格的操作,XLSX.utils.table_to_sheet(*)也能夠讀取出來,並且你打印出來的結果也能夠顯示出來,效果圖:
在這裏插入圖片描述
在這裏插入圖片描述
可以看到,excel中的表格也已經合併了。
但是實際的情況,客戶覺得這行字沒有居中,他就會向你嘮叨,爲啥不居中,所以我們現在解決文字不居中的問題。

設置樣式(居中,文字大小顏色,背景色…)

PS:這是我踩坑最多的地方…

這裏就不繞圈子了,設置樣式的話,上面的xlsx.full.min.js是無法生效的,
必須安裝xlsx-style

安裝xlsx-style

好像只有npm安裝,github我沒找到地址

npm install xlsx-style

同樣,安裝目錄下dist文件夾下有一個xlsx.full.min.js,嗯?名字一模一樣?怎麼用?好吧,無從下手,只好硬着頭皮引入了,注意,我將xlsx-style的js文件放在下方:
在這裏插入圖片描述
還有btn_export()方法要變一下,加一下樣式。
具體的單元格樣式說明可以看下這篇文章 xlsx-style單元格樣式參考表

function btn_export() {
        var table1 = document.querySelector("#table1");
        var sheet = XLSX.utils.table_to_sheet(table1);
        //這個就是修改格式的代碼
        sheet["A5"].s = { 
        	font: { sz: 13, bold: true, },
        	alignment: { 
        		horizontal: "center", vertical: "center", wrap_text: true 
        	} 
        };
        openDownloadDialog(sheet2blob(sheet),'下載.xlsx');
    }

改完之後,點擊運行,果不其然,報錯了:
在這裏插入圖片描述

原因是什麼呢,原因是2個js文件暴露出來的變量都叫‘XLSX’,但是xlsx-style這個js文件裏沒有XLSX.utils這個方法,而且xlsx-style這個js文件是後引入的,就把前面的XLSX給覆蓋了,所以報錯。
XLSX.utils裏面有很多可用的方法,但是按照這種方式無法進行調用:
在這裏插入圖片描述
你可能想到把2個js文件調換一下位置,但是結果是xlsx暴露的變量覆蓋了xlsx-style暴露的變量。你的樣式還是改變不了。

注意

如果你的導出功能 是傳入json格式或其他格式而沒有用到XLSX.utils的話,你只需使用xlsx-style的js,下面的內容可以忽略,下面的內容是講如何使xlsx和xlsx-style的js一起工作的。

不用XLSX.utils的方式

由於這2個js都是加密之後的內容,無法解讀,不能在這2個js上找到什麼有用的東西。好在在xlsx dist文件夾下找到了xlsx.extendscript.js,看這個文件就像個工具類,由於我上面用到了table_to_sheet方法,在xlsx.extendscript上面的搜索了一下,果然發現了這個方法,二話不說,將xlsx的js引用刪除,引入xlsx.extendscript:
在這裏插入圖片描述

運行。結果你應該已經猜到了,樣式並沒有發生改變。什麼原因呢,xlsx.extendscript.js暴露出來的變量仍然是’XLSX’,下面的變量還是覆蓋了上面的變量。
好在這個xlsx.extendscript.js不是壓縮版本,可以對內容進行修改,就把暴露出來的變量修改爲’XLSX2’吧。這樣我們只有在使用utils工具的時候纔用到xlsx.extendscript.js,其餘都用的是xlsx-style這個js,這樣總該可以了吧 。
修改完之後別忘了將XSLX.utils.table_to_sheet()改成XLSX2.utils.table_to_sheet()。
(不建議修改源碼,由於工作需要不修改源碼無法使用才做的修改)

function btn_export() {
        var table1 = document.querySelector("#table1");
        var sheet = XLSX2.utils.table_to_sheet(table1);
        sheet["A5"].s = {
            font: {
                sz: 13,
                bold: true,
                color: {
                    rgb: "FFFFAA00"
                }
            },
            alignment: {
                horizontal: "center",
                vertical: "center",
                wrap_text: true
            }
        };
        openDownloadDialog(sheet2blob(sheet), '下載.xlsx');
    }

運行:
在這裏插入圖片描述

可以看到,你所做的樣式更改已經生效了。
客戶需求增加:我想要前面幾行空出來,並且寫上打印公司名。
觀察xlsx.extendscript.js源碼,發現table_to_sheet,也就是parse_dom_table,並沒有設置起始行的參數,下面給出parse_dom_table的代碼:

function parse_dom_table(table, _opts) {
	var opts = _opts || {};
	if(DENSE != null) opts.dense = DENSE;
	var ws = opts.dense ? ([]) : ({});
	var rows = table.getElementsByTagName('tr');
	var sheetRows = opts.sheetRows || 10000000;
	var range = {s:{r:0,c:0},e:{r:0,c:0}};
	var merges = [], midx = 0;
	var rowinfo = [];
	var _R = 0, R = 0, _C, C, RS, CS;
	for(; _R < rows.length && R < sheetRows; ++_R) {
		var row = rows[_R];
		if (is_dom_element_hidden(row)) {
			if (opts.display) continue;
			rowinfo[R] = {hidden: true};
		}
		var elts = (row.children);
		for(_C = C = 0; _C < elts.length; ++_C) {
			var elt = elts[_C];
			if (opts.display && is_dom_element_hidden(elt)) continue;
			var v = htmldecode(elt.innerHTML);
			for(midx = 0; midx < merges.length; ++midx) {
				var m = merges[midx];
				if(m.s.c == C && m.s.r <= R && R <= m.e.r) { C = m.e.c+1; midx = -1; }
			}
			/* TODO: figure out how to extract nonstandard mso- style */
			CS = +elt.getAttribute("colspan") || 1;
			if((RS = +elt.getAttribute("rowspan"))>0 || CS>1) merges.push({s:{r:R,c:C},e:{r:R + (RS||1) - 1, c:C + CS - 1}});
			var o = {t:'s', v:v};
			var _t = elt.getAttribute("t") || "";
			if(v != null) {
				if(v.length == 0) o.t = _t || 'z';
				else if(opts.raw || v.trim().length == 0 || _t == "s"){}
				else if(v === 'TRUE') o = {t:'b', v:true};
				else if(v === 'FALSE') o = {t:'b', v:false};
				else if(!isNaN(fuzzynum(v))) o = {t:'n', v:fuzzynum(v)};
				else if(!isNaN(fuzzydate(v).getDate())) {
					o = ({t:'d', v:parseDate(v)});
					if(!opts.cellDates) o = ({t:'n', v:datenum(o.v)});
					o.z = opts.dateNF || SSF._table[14];
				}
			}
			if(opts.dense) { if(!ws[R]) ws[R] = []; ws[R][C] = o; }
			else ws[encode_cell({c:C, r:R})] = o;
			if(range.e.c < C) range.e.c = C;
			C += CS;
		}
		++R;
	}
	if(merges.length) ws['!merges'] = merges;
	if(rowinfo.length) ws['!rows'] = rowinfo;
	range.e.r = R - 1;
	ws['!ref'] = encode_range(range);
	if(R >= sheetRows) ws['!fullref'] = encode_range((range.e.r = rows.length-_R+R-1,range)); // We can count the real number of rows to parse but we don't to improve the performance
	return ws;
}

那自己加一個吧
可以看到,裏面的R變量 這是控制起始行的關鍵所在,好吧,我們再做一下修改:

var _R = 0, R = _opts.rowIndex || 0, _C, C, RS, CS;

這裏我們給_opts增加一個屬性rowIndex,在調用table_to_sheet方法的時候傳入這個屬性。下面是變更後的代碼:

function btn_export() {
        var table1 = document.querySelector("#table1");
        var opt = {
            rowIndex: 4
        }; //開頭空4行
        var sheet = XLSX2.utils.table_to_sheet(table1, opt);
        sheet["A1"] = {
            t: "s",
            v: '三鹿集團有限公司'
        }; //給A1單元格賦值
        sheet["A1"].s = {
            font: {
                name: '宋體',
                sz: 24,
                bold: true,
                underline: true,
                color: {
                    rgb: "FFFFAA00"
                }
            },
            alignment: { horizontal: "center", vertical: "center", wrap_text: true },
            fill: {
                bgColor: { rgb: 'ffff00' }
            }
        };
        //["!merges"]這個屬性是專門用來進行單元格合併的 
        sheet["!merges"].push({//如果不爲空push 爲空 = 賦值
            //合併單元格 index都從0開始
            s: { //s開始
                c: 0, //開始列
                r: 0 //開始行
            },
            e: { //e結束
                c: 3, //結束列
                r: 2 //結束行
            }
        });
        sheet["A9"].s = { //樣式
            font: {
                sz: 13,
                bold: true,
                color: {
                    rgb: "FFFFAA00"
                }
            },
            alignment: {
                horizontal: "center",
                vertical: "center",
                wrap_text: true
            }
        };
        openDownloadDialog(sheet2blob(sheet), '下載.xlsx');
    }

運行結果:
在這裏插入圖片描述
可以看到,你所做的更改生效了。

客戶又提新需求了,要加上2個字段,身份證號和手機號。
這還不簡單?加上2個字段不就好了。2分鐘搞定,導出:
在這裏插入圖片描述
???
身份證號怎麼變成了科學計數法,什麼鬼(後來發現百分比也會直接給你換算成0~1的小數,統計沒法搞)
怎麼回事?還是parse_dom_table的傑作!
注意這一行:

else if(!isNaN(fuzzynum(v))) o = {t:'n', v:fuzzynum(v)};

意思是隻要從td的text裏讀取到的值,只要轉換之後是一個number,(不管你是string類型),都會給你來一個fuzzynum(v),轉換成一個number類型。
做下修改,結果:

function parse_dom_table(table, _opts) {
	var opts = _opts || {};
	if(DENSE != null) opts.dense = DENSE;
	var ws = opts.dense ? ([]) : ({});
	var rows = table.getElementsByTagName('tr');
	var sheetRows = opts.sheetRows || 10000000;
	var range = {s:{r:0,c:0},e:{r:0,c:0}};
	var merges = [], midx = 0;
	var rowinfo = [];
	var _R = 0, R = _opts.rowIndex || 0, _C, C, RS, CS;
	for(; _R < rows.length && R < sheetRows; ++_R) {
		var row = rows[_R];
		if (is_dom_element_hidden(row)) {
			if (opts.display) continue;
			rowinfo[R] = {hidden: true};
		}
		var elts = (row.children);
		for(_C = C = 0; _C < elts.length; ++_C) {
			var elt = elts[_C];
			if (opts.display && is_dom_element_hidden(elt)) continue;
			var v = htmldecode(elt.innerHTML);
			for(midx = 0; midx < merges.length; ++midx) {
				var m = merges[midx];
				if(m.s.c == C && m.s.r <= R && R <= m.e.r) { C = m.e.c+1; midx = -1; }
			}
			/* TODO: figure out how to extract nonstandard mso- style */
			CS = +elt.getAttribute("colspan") || 1;
			if((RS = +elt.getAttribute("rowspan"))>0 || CS>1) merges.push({s:{r:R,c:C},e:{r:R + (RS||1) - 1, c:C + CS - 1}});
			var o = {t:'s', v:v};
			var _t = elt.getAttribute("t") || "";
			if(v != null) {
				if(v.length == 0) o.t = _t || 'z';
				else if(opts.raw || v.trim().length == 0 || _t == "s"){}
				else if(v === 'TRUE') o = {t:'b', v:true};
				else if(v === 'FALSE') o = {t:'b', v:false};
        //else if(!isNaN(fuzzynum(v))) o = {t:'n', v:fuzzynum(v)};
        else if(!isNaN(fuzzynum(v))) o = {t:'s', v:v};//不自動格式化number類型
				else if(!isNaN(fuzzydate(v).getDate())) {
					o = ({t:'d', v:parseDate(v)});
					if(!opts.cellDates) o = ({t:'n', v:datenum(o.v)});
					o.z = opts.dateNF || SSF._table[14];
				}
			}
			if(opts.dense) { if(!ws[R]) ws[R] = []; ws[R][C] = o; }
			else ws[encode_cell({c:C, r:R})] = o;
			if(range.e.c < C) range.e.c = C;
			C += CS;
		}
		++R;
	}
	if(merges.length) ws['!merges'] = merges;
	if(rowinfo.length) ws['!rows'] = rowinfo;
	range.e.r = R - 1;
	ws['!ref'] = encode_range(range);
	if(R >= sheetRows) ws['!fullref'] = encode_range((range.e.r = rows.length-_R+R-1,range)); // We can count the real number of rows to parse but we don't to improve the performance
	return ws;
}

將轉換的語句註釋掉,重寫這行代碼,如果是number類型,不做任何修改,該是什麼值還是什麼值。
現在再重新運行,結果:
在這裏插入圖片描述可以看到,數字能夠正常顯示了。但是這個單元格好像並不會自動展開,永遠都這麼大,xlsx-style 也提供了控制單元格寬度的方法:

sheet["!cols"] = [{
            wpx: 70
        }, {
            wpx: 70
        }, {
            wpx: 70
        }, {
            wpx: 70
        }, {
            wpx: 150
        }, {
            wpx: 120
        }]; //單元格列寬

注意,設置單元格列寬要從第一行開始設置
結果:
在這裏插入圖片描述
完整前端代碼:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <table id="table1" border="1" cellspacing="0" cellpadding="0">
        <thead>
            <tr>
                <td>序號</td>
                <td>姓名</td>
                <td>年齡</td>
                <td>興趣</td>
                <td>身份證號</td>
                <td>手機號</td>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>張三</td>
                <td>18</td>
                <td>打遊戲</td>
                <td>320322184087562589</td>
                <td>1374569821</td>
            </tr>
            <tr>
                <td>2</td>
                <td>李四</td>
                <td>88</td>
                <td>看電影</td>
                <td>420322184087562589</td>
                <td>2374569821</td>
            </tr>
            <tr>
                <td>3</td>
                <td>王五</td>
                <td>81</td>
                <td>睡覺</td>
                <td>520322184087562589</td>
                <td>3374569821</td>
            </tr>
            <tr>
                <td colspan="4">這是一個合併單元格</td>
            </tr>
        </tbody>
    </table>

    <button id="btn" onclick="btn_export()">導出</button>
</body>
<script src="js/xlsx.extendscript.js"></script>
<script src="js/xlsx-style/xlsx.full.min.js"></script>

<script src="js/export.js"></script>
<script>
    function btn_export() {
        var table1 = document.querySelector("#table1");
        var opt = {
            rowIndex: 4
        }; //開頭空4行
        var sheet = XLSX2.utils.table_to_sheet(table1, opt);
        sheet["A1"] = {
            t: "s",
            v: '三鹿集團有限公司'
        }; //給A1單元格賦值
        sheet["A1"].s = {
            font: {
                name: '宋體',
                sz: 24,
                bold: true,
                underline: true,
                color: {
                    rgb: "FFFFAA00"
                }
            },
            alignment: {
                horizontal: "center",
                vertical: "center",
                wrap_text: true
            },
            fill: {
                bgColor: {
                    rgb: 'ffff00'
                }
            }
        };
        //["!merges"]這個屬性是專門用來進行單元格合併的 
        sheet["!merges"].push({ //如果不爲空push 爲空 = 賦值
            //合併單元格 index都從0開始
            s: { //s開始
                c: 0, //開始列
                r: 0 //開始行
            },
            e: { //e結束
                c: 3, //結束列
                r: 2 //結束行
            }
        });
        sheet["A9"].s = { //樣式
            font: {
                sz: 13,
                bold: true,
                color: {
                    rgb: "FFFFAA00"
                }
            },
            alignment: {
                horizontal: "center",
                vertical: "center",
                wrap_text: true
            }
        };

        sheet["!cols"] = [{
            wpx: 70
        }, {
            wpx: 70
        }, {
            wpx: 70
        }, {
            wpx: 70
        }, {
            wpx: 150
        }, {
            wpx: 120
        }]; //單元格列寬

        openDownloadDialog(sheet2blob(sheet), '下載.xlsx');
    }
</script>

</html>

demo源碼

完整實例

總結

  1. 不是特殊情況不建議修改源碼
  2. 因爲畢竟修改了代碼,所以這種方法只能面向小衆
  3. 聽過收費版功能很全,建議如果有需要的話還是購買收費版本,但是地址沒找到…
  4. 可以根據自己需求對xlsx源碼進行修改,以便滿足自己工作的需求。但是這樣較難以維護,如何取捨還是自行斟酌。
  5. 我這裏只列取了我實際工作中所需要的功能,xlsx的功能很豐富,有空可以多琢磨琢磨。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章