純前端處理excel數據
問題所在
由於公司一直有關於活動、會議等專題前端頁面的需求。並且之中會有會議議程,相關表格等。處理這類表格需求,之前的做法有兩種:
1.直接使用設計師的設計切圖
2.前端將會議議程寫進頁面中
其實兩種方法都可以,但有個問題是這樣的表格會進行頻繁的更改。比如出席人員、會議項目等,隨時有進行修改、刪除的可能。這樣每次修改都會牽涉相關的人力,每次的小修改會造成大量的人力浪費。
如果時第一種方式,前端開發人員會減少一定的工作量。但對於設計人員,據他們的發聵情況是說這樣會非常麻煩。第二種方式前端修改也是相當繁瑣。所以就想能不能想辦法解決這個問題。
尋找解決方案
解決方案應該滿足以下條件:
- 表格/列表以代碼方式實現,不再使用一張切圖(這樣也是前端偷懶的做法)
- 設計只負責出設計效果圖,前端根據設計搞實現效果
- 將數據與UI主見相互分離,不會因為數據的更改而使UI不能使用
但是存在一個問題,后期數據的修改如何來處理。是需求修改后,產品再與開發人員溝通進行數據修改。這樣其實就沒任何的改進,怎樣才能讓產品可以輕松、方便的進行數據修改。這樣就想到了一個方法——excel,產品對於excel的操作肯定是非常熟悉的了,也便於將這個數據修改的工作交到他們手中。
如何實現
首先,由於后端小伙伴工作的繁重。對於excel數據的處理沒法讓他們進行技術支持,所以只能由前端來處理了。
那我在瀏覽器的這個環境下如何來處理excel數據呢?javascript當然是可以處理excel數據文件的,但是在瀏覽器這樣的環境下怎么來讀取excel這種復雜的文件呢。所以只能去找找有沒有這樣的插件
在github上找到了一個開源庫xlsx,可以通過npm方式來安裝。
npm install xlsx --save
在自己的html文件里面添加對js文件的引用
<script src="./node_modules/xlsx/dist/jszip.js"></script>
<script src="./node_modules/xlsx/dist/xlsx.js"></script>
有一個問題,這種數據是希望將它持久化顯示的。但又沒有后台的支持,只能完全依靠表格文件。所以表格就是一個持久化的數據。
我的思路時用ajax異步去請求文件,相應地就可以讀取表格文件數據。只要能拿到數據,進入到javascript環境就有接下來的故事。剛好開源庫xlsx也支持這樣的方式:
/* set up XMLHttpRequest */
var url = "test_files/formula_stress_test_ajax.xlsx";
var oReq = new XMLHttpRequest();
oReq.open("GET", url, true);
oReq.responseType = "arraybuffer";
oReq.onload = function(e) {
var arraybuffer = oReq.response;
/* convert data to binary string */
var data = new Uint8Array(arraybuffer);
var arr = new Array();
for(var i = 0; i != data.length; ++i) arr[i] = String.fromCharCode(data[i]);
var bstr = arr.join("");
/* Call XLSX */
var workbook = XLSX.read(bstr, {type:"binary"});
/* DO SOMETHING WITH workbook HERE */
}
oReq.send();
這樣我就可以將表格數據拿出來處理了。
最終的UI活動議程可能是這樣

也可能是這樣的表格

所以不能固化到將數據僅僅整理成table表格數據,我們需要將數據處理成前端便於處理的json數據。這樣就不怕設計的設計如何變化。
直接上代碼
/**
* [description]
* excel配置會議議程
* @author mao
* @version 1
* @date 2016-09-28
*/
var excel = (function(mod) {
/**
* [checkColumn description]
* 檢測是幾列數據
* @author mao
* @version 1
* @date 2016-10-12
* @param {object} workbook excel數據
* @return {[object]} 結果數據
*/
function checkColumn(workbook) {
var result = workbook.Sheets[(workbook.SheetNames)[0]],
column,
data;
//判斷是列excel數據
column = ((result['!ref']).split(':')[1]).charAt(0);
//處理成hash結構
data = processOrigin(result, column);
return data;
}
/**
* [processOrigin description]
* @author mao
* @version 1
* @date 2016-10-12
* @param {object} result 待處理excel數據
* @param {string} column 有多少列數據
* @return {Object} 結果數據
*/
function processOrigin(result, column) {
var merges = result['!merges'] || [], //合並表格的位置信息
obj,response;
//生成對應的幾項hash數據
switch(column) {
case 'B' :
obj = {
_1:{}
};
break;
case 'C' :
obj = {
_1:{},
_2:{}
};
break;
case 'D' :
obj = {
_1:{},
_2:{},
_3:{}
};
break;
case 'E' :
obj = {
_1:{},
_2:{},
_3:{},
_4:{}
};
break;
default: break;
}
//拿到excel初始的hash數據
for(var i in result) {
if(i.charAt(0) === '!' || i.charAt(0) === 'A') continue;
switch(i.charAt(0)) {
case 'B':{
var key = i.slice(1,i.length);
obj._1[key] = result[i].v;
break;
}
case 'C':{
var key = i.slice(1,i.length);
obj._2[key] = result[i].v;
break;
}
case 'D':{
var key = i.slice(1,i.length);
obj._3[key] = result[i].v;
break;
}
case 'E':{
var key = i.slice(1,i.length);
obj._4[key] = result[i].v;
break;
}
default:break;
}
}
//合並項
response = mergeColumn(obj, merges);
return response;
}
/**
* [mergeColumn]
* description
* @author mao
* @version 1
* @date 2016-10-12
* @param {obj} obj 取出的excel數據
* @param {Object} merges 合並單元格的起始坐標
* @return {Object} 補全后的單元格數據
*/
function mergeColumn(obj, merges) {
//判斷是否只為一列
var _keys = [];
for(var i in obj) {
_keys.push(i);
}
if(_keys.length === 1) {
return obj;
}
//驗證是否有合並
if(merges.length === 0) {
console.log('merges is empty');
return obj;
}
//將數據處理成全項目的hash
for(var i = 0; i < merges.length; i++) { //縱向合並
if(merges[i].e.c == merges[i].s.c) {
var start = merges[i].s.r + 1,
end = merges[i].e.r + 1,
sub = merges[i].e.c, //起點x坐標
range = end - start,
origin = obj['_' + sub][start];
//起始點數據
obj['_' + sub][start] = {
_v: origin,
_w: 'row',
_s: true,
_c: (range + 1)
}
//補全被合並項
for(var j = 1; j <= range; j++) {
start ++;
obj['_' + sub][start] = {
_v: origin,
_w: 'row',
_s: false,
_c: (range + 1)
}
}
} else { //橫向的合並
var start = merges[i].s.c,
end = merges[i].e.c,
sub = merges[i].e.r + 1, //起點y坐標
range = end - start,
origin = obj['_' + start][sub];
//起始點數據
obj['_' + start][sub] = {
_v: origin,
_w: 'col',
_s: true,
_c: (range + 1)
}
//補全被合並項
for(var j = 1; j <= range; j++) {
start ++;
obj['_' + start][sub] = {
_v: origin,
_w: 'col',
_s: false,
_c: (range + 1)
}
}
}
}
return obj;
}
/**
* [toArray description]
* @author mao
* @version 1
* @date 2016-10-12
* @param {Object} obj 待處理的hash
* @return {array} 處理成的數組
*/
function toArray(obj) {
var keys = [],
data = obj._1,
res = [];
//獲取key值
for(var i in obj) {
keys.push(i);
}
//處理成數組
for(var i in data) {
var current = {};
for(var j = 0; j < keys.length; j++) {
current[keys[j]] = obj[keys[j]][i];
}
res.push(current);
}
return res;
}
/**
* [createXHR]
* 創建一個xhr
* @author mao
* @version 1
* @date 2016-09-26
* @return {object} xhr
*/
function createXHR() {
if(window.XMLHttpRequest) {
return new XMLHttpRequest();
} else if(window.ActiveXObject) { //ie6
return new ActiveXObject('MSXML2.XMLHTTP.3.0');
} else {
throw 'XHR unavailable for your browser';
}
}
/**
* [transferData]
* 請求excel文件請求
* @author mao
* @version 1
* @date 2016-09-28
* @param {Function} option.dataRender 回調函數,處理結果數據
* @param {string} option.url xlsx文件請求地址
*/
mod.transferData = function(option) {
//新建xhr
var oReq = createXHR(),
resultData;
//建立連接
oReq.open("GET", option.url, true);
//判斷是否為低版本的ie,處理返回
if(typeof Uint8Array !== 'undefined') {
oReq.responseType = "arraybuffer";
oReq.onload = function(e) {
if(typeof console !== 'undefined') console.log("onload", new Date());
var arraybuffer = oReq.response;
var data = new Uint8Array(arraybuffer);
var arr = new Array();
for(var i = 0; i != data.length; ++i) {
arr[i] = String.fromCharCode(data[i]);
}
//處理數據
var wb = XLSX.read(arr.join(""), {type:"binary"});
//數據放入回調
option.dataRender(toArray(checkColumn(wb)));
};
} else {
oReq.setRequestHeader("Accept-Charset", "x-user-defined");
oReq.onreadystatechange = function() {
if(oReq.readyState == 4 && oReq.status == 200) {
var ff = convertResponseBodyToText(oReq.responseBody);
if(typeof console !== 'undefined') {
console.log("onload", new Date());
}
//處理數據
var wb = XLSX.read(ff, {type:"binary"});
//數據放入回調
option.dataRender(toArray(checkColumn(wb)));
}
};
}
//發送請求
oReq.send();
}
/**
* [check_undefind description]
* @author mao
* @version 1
* @date 2016-10-13
* @param {string} data 數據
* @return {string} 返回空或數據本身
*/
function check_undefind(data) {
if(!data) {
return '';
} else {
if(typeof data != 'number') {
//檢測特殊字符
if(data.indexOf(' ') != -1) {
return data.split(' ').join('<br/>');
} else {
return data;
}
} else {
return data;
}
}
}
/**
* [renderHTML description]
* @author mao
* @version 1
* @date 2016-10-13
* @param {object} table 最終數據
* @return {string} 渲染的dom
*/
mod.renderHTML = function(table) {
var html = '';
for(var i = 0; i <table.length; i++) {
html += '<tr>';
for(var j in table[i]) {
var item = table[i][j];
if(typeof item === 'object') {
switch(item._w) {
case 'col': {
if(item._s) {
html += '<td colspan="'+item._c+'">'+check_undefind(item._v)+'</td>';
}
break;
}
case 'row': {
if(item._s) {
html += '<td rowspan="'+item._c+'">'+check_undefind(item._v)+'</td>';
}
break;
}
default:break;
}
} else {
html += '<td>'+check_undefind(item)+'</td>';
}
}
html += '</tr>';
}
return html;
}
return mod;
})(excel || {})
處理出來的數據如圖,

這樣的結果數據可以很友好地裝載入UI結構當中,這樣最后議程修改就只需要產品修改excel數據。很大程度上節省了流程以及人力成本。
兼容低版本瀏覽器可引入以下文件

