纯前端处理excel数据
纯前端处理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数据。很大程度上节省了流程以及人力成本。
兼容低版本浏览器可引入以下文件