移动端浏览器导出excel的神奇之旅

2,299 阅读2分钟

故事还得从一个朴实无华的需求说起,产品要求在移动端H5应用中增加导出excel的功能,年底前要上线的,我一想导出excel,这我不是在PC端写过N次了嘛,简直小菜一碟嘛。我就是加班,也不要后端开一个接口(开个玩笑 O(∩_∩)O哈哈~)。

后端大佬给出的接口是这样的:

路径: /api/project/statistics/excelProject

请求类型: Get

请求头: Content-Type:application/x-www-form-urlencoded

需要的参数: beginDate endDate keyword

我一看这个接口,既然是get请求,那么将参数拼接在地址后面,岂不是可以直接通过window.open来打开新窗口触发get请求下载文件了。于是就有了第一种方法:

let hostUrl = `http://${location.host}`;
window.open(`${hostUrl}/api/project/statistics/excelProject?beginDate=${this.beginDate}&endDate=${this.endDate}&keyword =${this.keyword} `)

很快啊,一下子就在PC端上的浏览器实现了导出excel的功能,不多不少,功能刚刚好,这导出excel多是一件美事呀。

乍一看还真简单呢!然后我准备在移动端上进行测试,结果呢,拉了。。。

首先是安卓微信浏览器,使用window.open会直接打开一个新窗口,然而这个新窗口并不是微信浏览器里的,而是在手机上默认的浏览器打开。那么问题来了,用户并没有在手机默认浏览器登录过,然后触发导出接口必然会对用户权限进行验证所以就如下所示了:

既然安卓系统上的微信浏览器无法实现,那么可以试试手机默认的浏览器呀(这里我用的是小米)毕竟打开新窗口时用户的权限信息都是在的呀。然后,我又进行了测试。结果如下所示,

虽然有了下载的弹框,但是下载居然是bin文件。我百思不得姐,于是又去冲了会浪,综合网上的说法,我断定是接口响应头部content-type类型不对才导致的问题。于是,我用抓包工具看下了这个接口的响应。你们有人可能会好奇,在PC端的chrome中的network不是可以直接看接口的请求记录嘛。当然我也想这么操作呀,只不过当我触发window.open时,浏览器貌似闪了一下就弹了个下载提示框就没了,请求记录压根没有在原来窗口的network中记录下来。所以我只能请出抓包工具了呀。然后一看抓包截图就发现了: 这里居然用的是

Content-Type: application/x-download;charset=UTF-8`

于是我让后端将响应头部改成了这样之后,才算在安卓上的浏览器能正常下载excel

Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`

中途后端也曾用过'application/vnd.ms-excel',但是兼容效果并不理想,虽然在安卓机的浏览器上确实能正确弹出下载excel文件的提示,但是在ios浏览器中压根连excel都无法预览了,更别提弹框提示下载了。

前面介绍的都是安卓机的试验,那现在看看ios上的表现,首先试试ios微信浏览器呢?这里就挺神奇的,我本以为会同安卓一样打开了默认浏览器Safari,但是在触发window.open()时,并没有在手机默认的safari浏览器中打开新窗口而是在微信浏览器内部打开了新窗口,那么此时用户的权限便是正常的。我一想,这不就代表会进行下载了嘛!!!然后离谱的事情发生了,新窗口只是中正确展示了excel文件信息,但是并没有下载提示(貌似只有用户在第一次进行下载时,才会有下载提示)

之后,我又在ios默认浏览器Safari上进行了测试,测试结果和ios微信浏览器的运行效果是一样的:同样只是在新窗口中展示了excel文件却并没有下载的提示框。

因此我得出结论:window.open是没法完整实现安卓和ios各自的微信浏览器以及手机默认浏览器下载excel的功能,综合表现很差


既然window.open走不通,那么我准备用a标签来试试。

由于后端给出的接口是get请求。所以我可以尝试两种方式来进行a标签的下载文件。 一种便是将get请求返回的文件流转成url;另一种就是直接使用get请求地址。代码分别如下:

// 导出项目excel
excelProject(params) {
  return get('api/api/project/statistics/excelProject', params,{
    responseType: 'blob',
  })
},
// 导出人员excel
excelUser(params) {
  return get('api/api/project/statistics/excelUser', params,{
    responseType: 'blob',
  })
},


async outputExcel(type) {
    if(type === '项目') {
        const data = await this.excelProject();
        this.download(data,'项目');
    } else if (type === '人员') {
        const data = await this.excelUser();
        this.download(data, '人员');
    }
},
download(data,type) {
    let eleLink = document.createElement('a');
    let url = window.URL.createObjectURL(new Blob([data]));
    eleLink.href = url;
    eleLink.download = `${type}统计.xlsx`;
    document.body.appendChild(eleLink);
    eleLink.style.display = 'none';
    eleLink.click();
    document.body.removeChild(eleLink)
    window.URL.revokeObjectURL(eleLink);
},


这种方法在ios微信浏览器和安卓微信浏览器中的效果一样,点击之后只是进行了接口请求,虽然也生成了a标签但是点击后并不能导出excel。

然而在部分移动端浏览器中可以实现比如Safari、小米浏览器等可以正常提示下载并导出excel。

outputExcel(type) {
    this.download(type);
},

download(type) {
    let hostUrl = `http://${location.host}`;
    // 项目
    const projectExcelUrl = `${hostUrl}/api/project/statistics/excelProject?beginDate=${this.beginDate}&endDate=${this.endDate}&keyword =${this.keyword}`;
    // 人员
    const userExcelUrl = `${hostUrl}/api/project/statistics/excelUser?beginDate=${this.beginDate}&endDate=${this.endDate}&keyword =${this.keyword}`;
    let link = document.createElement('a')
    link.style.display = 'none'
    link.href = type === '项目' ? projectExcelUrl: userExcelUrl;
    link.setAttribute('download', `${type}.xlsx`)
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
},

使用绝对地址的方法的话,ios微信浏览器只会在新页面中展示excel,但没有提示下载。但在安卓微信浏览器中触发方法时,会在手机默认浏览器打开新窗口,那么会因为没有用户登录信息而提示未登录就跟window.open产生的结果是一样的。

在测试iOS默认浏览器Safari中可以正常展示且提示了下载,安卓手机默认浏览器(小米)可以正常提示下载。

总结a标签的各种浏览器测试结果:文件流转URL比直接使用地址的表现差,a标签下载excel在手机默认浏览器中有很好的表现,但是在微信浏览器中依然无法实现下载功能。综合表现比通过window.open更好些。

本来是想实现在微信浏览器中实现直接下载excel的功能,但是经过多次实验之后,发现微信浏览器确实在下载文件方面确实表现不够好。因此最后和同事讨论的解决办法是:使用a标签进行下载excel,判断用户登录的浏览器是否是微信浏览器,是的情况下,我们给出提示:通过点击右上角(···)并在浏览器(默认)中打开。而用户一开始就是在默认浏览器打开时,我们就不做任何处理。