最近接到这么个需求,要把 <table>
显示的数据导出成 Excel。类似的需求并不稀罕,过去我通常用 PHP 输出 .csv 文件。不过这次似乎不太合适:作为数据源的表格允许用户有一些筛选和排序的动作,与原始数据显示有区别,传递操作比较麻烦;另外 .csv 文件的功能受限严重,难以扩展。所以我准备尝试下别的做法。
Google之,发现 HTML5 又成了一座分水岭。之前在IE浏览器下,用户可以利用 ActiveXObject
创建 Excel.application
对象来处理。后来 Excel 开放标准,可以导出 xml 格式的文件,dataURI
就有了用武之地,导出 <table>
数据并保存为 Excel 有了更好的选择。
(以下内容与 StackOverflow中的答案有重合,那个3条赞同的我认为是最佳答案,可惜我没法顶他……)
准备工作
- 创建一个空白的Excel文档
- 另存为“XML表格”,XML 格式
- 好了,模版搞定
或者,直接复制下面一段(这一段我使用了Handlebars模版,以便将来填充数据)
template = ‘<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"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{{worksheet}}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>{{#each tables}}<table>{{{this}}}</table>{{/each}}</body></html>';
复制表格数据
复制数据比较简单了。如前面模版所示,这里我很野蛮的直接复制 <thead>
和 <tbody>
的全部代码,填充内容。当然为了体现用户操作,我只复制显示的 <tr>
。这里需要注意的是,jQuery 判断一个dom
是否处于显示状体基于以下3点:
display:none
- 表单元素,
type="hidden"
- 宽高为0
- 父级以上节点不显示,自己也不会显示
所以,不能先 .clone()
再 .find(':hidde').remove()
,因为添加到主 Dom 树之前,节点宽高都是0,也就会被认为还没显示,这下就都干掉了。
输出内容
套用模版之后,我们就有了完整的表格数据。接下来,我们需要把其转换成 Base64 格式,以便套用 dataURI
输出。于是便要使用 btoa
这个函数(将二进制数据转换成 base64 格式的字符串),不过注意,这个函数不能直接转换普通 unicode 字符,不然大多数浏览器都会抛出异常。所以需要先经过两步转换:
function base64(string) {
return window.btoa(unescape(encodeURIComponent(string)));
}
(MDN 还推荐了 另外一种做法,通过 Typed Array 做中介,我没有实操,有兴趣的可以试下。)
然后配上 data 头和 mimetype,就可以触发下载了:
var uri = 'data:application/vnd.ms-excel;base64,';
location.href = uri + base64(template(tables));
提升体验
貌似到这里就完成了,不过作为一名挂职产品总监的码农,我很难容忍下载的文件文件名是“下载”,而且还没有扩展名(Windows 8 下,Windows 7 和 Mac 下会有.xls的扩展名,我认为和装的软件注册 mime 类型有关)。
这是个用在内部管理后台的需求,我之前曾要求大家必须使用 Chrome 访问后台;而且我知道,Chrome 已经支持 <a>
里的 download
属性。那么这就好办了,因为 onclick
事件会先于系统默认行为触发,所以我可以在这个事件的处理函数中将生成的 Base64 放在被点击按钮的 href
里,并将其 download
属性设为容易理解的“某年某月末日至某年某月某日广告数据分析.xls”。至此,此项功能宣告圆满。
HTML部分(使用了Bootstrap和Handlebars):
<a href="#" title="点击下载" class="btn btn-primary export-button" download="{{start}}至{{end}}广告数据分析.xls"><i class="icon-download-alt icon-white"></i> 导出</a>
JavaScript 部分
tableToExcel: function (tableList, name) {
var tables = []
, uri = 'data:application/vnd.ms-excel;base64,'
, template = Handlebars.compile('<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"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{{worksheet}}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>{{#each tables}}<table>{{{this}}}</table>{{/each}}</body></html>');
for (var i = 0; i < tableList.length; i++) {
tables.push(tableList[i].innerHTML);
}
var data = {
worksheet: name || 'Worksheet',
tables: tables
};
return uri + base64(template(data));
},
exportHandler: function (event) {
var tables = this.$('table')
, table = null;
tables.each(function (i) {
var t = $('<table><thead></thead><tbody></tobdy></table>');
t.find('thead').html(this.tHead.innerHTML);
t.find('tbody').append($(this.tBodies).children(':visible').clone());
t.find('.not-print').remove(); // not-print 是@media print中不会打印的部分
t.find('a').replaceWith(function (i) { // 表格中不再需要的超链接也移除了
return this.innerHTML;
});
table = table ? table.add(t) : t;
});
event.currentTarget.href = Dianjoy.utils.tableToExcel(table, '广告数据');
}
尾声
说是圆满,其实也不尽然,因为 URL 有 2M 的长度限制,遇到真正的大表仍然可能出问题(我没实测)。
最后例行吐槽:老板(领导)想提升工作效率,必须考虑员工的日常软件:不许用乱七八糟的浏览器,统一 Chrome;360 一定禁用(最近遇到 N 起升级 Chrome Dev 30 版导致各种 bug 的问题);全部装 Windows 8(自带杀毒,几乎所有外设秒配)。能做到这几点,公司办公效率提升1倍不止。
欢迎吐槽,共同进步