最近公司須要將幾張統計表格導出到excel,因爲公司現有導出excel功能是先後端配合的導出,以爲麻煩,因此想找一個純前端導出的工具,最後找到了js-xlsx,評價仍是挺高的,可是中文文檔沒找到,百度也沒有找到一個比較全面的教程,因此踩了不少坑,本身記錄下,方便之後使用。javascript
因爲我業務只用到將table標籤內的內容導出到excel,因此只會寫如何將一個table元素裏的內容導出到excel。也能夠經過json導出,貌似還會更簡單些。html
GitHub地址
npm安裝前端
npm install xlsx
安裝後dist文件夾下有一個文件xlsx.full.min.js,就是它了,引入到項目中java
先上代碼webpack
<!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>
運行效果git
導出結果:github
你可能注意到了,我這裏引入了一個export.js文件,這個export.js文件裏面只有2個方法,就是上面代碼用到的openDownloadDialog(sheet2blob(sheet),'下載.xlsx');
這是export.js的代碼:web
// 將一個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個方法是網上當的,原文地址。做者寫的挺好,也是從這裏找到了頭緒。npm
若是你的table標籤內有合併單元格的操做,XLSX.utils.table_to_sheet(*)也可以讀取出來,而且你打印出來的結果也可以顯示出來,效果圖:
能夠看到,excel中的表格也已經合併了。
可是實際的狀況,客戶以爲這行字沒有居中,他就會向你嘮叨,爲啥不居中,因此咱們如今解決文字不居中的問題。json
這裏就不繞圈子了,設置樣式的話,上面的xlsx.full.min.js是沒法生效的,
必須安裝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暴露的變量。你的樣式仍是改變不了。
因爲這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>
github地址 完整實例