7
头图

问题描述

项目里需要实现一个导出csv的功能,这是个老生常谈的需求,而且我们使用的是iview的组件库,按道理说实现起来应该简单,但实则不然,我在做的时候遇到了一些问题。受限于请求需要token后端分页接口性能等原因不得不放弃iview的导出方式。所以我需要寻找一种可行的、合理的、优雅的导出方案,那就是Data URI Scheme

方案实现

方案介绍

Data URI Scheme是利用HTML标签的hrefsrc属性来实现的。他看起来像是这样的:

<img src="data:image/png;base64,iVBORw0KGgoAAA
ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU
5ErkJggg==" alt="Red dot" />

或者

<a href="data:text/csv,something">download</a>

按照这种方案的介绍,我们把要导出的数据拼接在href指定位置就能实现导出的需求,代码实现看起来像这样:

<a href="" download="export.csv" id="export_csv" style="display='none'">download</a>
function export_csv (data) {
    $('#export_csv').href = 'data:attachment/csv,' + encodeURI(data);
    $('#export_csv').click();
    setTimeout(function () {
        $('#export_csv').href = '';    
    })
}
export_csv(csv_data_str);

测试发现,妥妥的,没毛病。

存在问题

在实践中这个方案是有限制的、不安全的:在chrome的实现中Data URI Scheme允许的URL的最大限制为2MB(其他浏览器这里不做讨论)。
一开始并不知道是超过2MB才会出问题,只是发现:
当在下载的文件比较大(超过2.7MB)的时候Chrome会报这样的错误:

下载
失败-网络错误

clipboard.png

后来google到这个限制是2MB,因为没有官方文档说明,感觉2MB的说法不是很确定,所以去扒了Chromium源码,找到了相关代码:

const size_t kMaxURLChars = 2 * 1024 * 1024;
...
if (!iter->ReadString(&s) || s.length() > url::kMaxURLChars) {
    *p = GURL();
    return false;
}
  1. 变量声明部分源码链接
  2. 变量引用部分源码链接

2MB的限制算是实锤了,同时发现2010年就有人在Chromium论坛提出2MB太小了了,但是一直讨论到2019年也没有明显的改善(只是改了图片部分)。唉,chromium不改,我们能怎么办呢?

方案改进

chromium不改,那我们只能自己想办法了,于是有大牛提出来使用URL.createObjectURL + Blob来突破这个限制。
借助Blob对象和URL.createObjectURL我们可以得到一个很短的、而且几乎与内容长度无关的URL:

blob:https://xxx.com/0bde569d-20a2-4085-95e6-dcec242962c6

这样就能突破Chrome对Data URI Scheme URL大小的限制了。
当然呢,我没用过URL.createObjectURL这个方法,也没用过Blob对象,所以我们要看看浏览的支持情况

clipboard.png

恩,看起来没有问题,那我们来看看代码实现。

<a href="" download="export.csv" id="export_csv" style="display='none'">download</a>
function export_csv (data) {
    const BOM = '\uFEFF';
    let blob_obj = new Blob([BOM + data], {type: 'text/csv'});
    let download_url = URL.createObjectURL(blob_obj);
    $('#export_csv').href = download_url;
    $('#export_csv').click();
    setTimeout(function () {
        // 通过createObjectURL创建的url需要通过revokeObjectURL()来释放
        URL.revokeObjectURL(download_url);
        $('#export_csv').href = '';
    })
}
export_csv(csv_data_str);

如此,问题解决了,这样就不怕超过2MB的CSV的导出了。

但是Chrome对Blob对象的大小有限制吗?

Good question !

我在chromium Blob的说明文档中找到一个表:

DeviceRamIn-Memory LimitDiskDisk LimitMin Disk Availability
Cast512 MB102 MB000
Android Minimal512 MB5 MB8 GB491 MB10 MB
Android Fat2 GB20 MB32 GB1.9 GB40 MB
CrOS2 GB409 MB8 GB4 GB0.8 GB
Desktop 323 GB614 MB500 GB50 GB1.2 GB
Desktop 644 GB2 GB500 GB50 GB4 GB

从这个表中,大概可以看出来在In-Memory Storage的时候桌面版64位Chrome Blob的上限为2GB(在Chrome 57上限是500MB)。所以现在看来这种方法应该是安全的。至此,这个问题算是完整的解决了。

iview的实现

另外,在我写这篇文章的时候我发现iviewexport-csv方法也是按照这个方案实施的,而且做了更多兼容,可以方便大家参考。但他在资源释放的地方做的还需改进,也希望大家注意。

参考文档


aqiongbei
2k 声望281 粉丝

人生路上,你走的每一步都算数