背景:
新上线的一个需求,研发期间,大家都做得比较辛苦。
同时,收到用户反馈功能功能不好用
问题描述:
反馈的问题是,导出的文件名像是乱码,看不懂。
比如多导出几次,导出的文件多了,就不容易找到想要的那个。
如果下载一个用户再自己重新命名一下,又会影响效率。
根据文件名不知道里面的内容
这就很烦了, 不改下名,不好找导出的文件。 改吧,又太麻烦。
用户又能拿这个功能怎么样,只能吐槽了
这是一个非功能的体验问题。
直接原因:
浏览器使用了默认的命名策略,如果没有指定下载文件名那么浏览器会这样这样:
将url上的非法字符去掉,然后拼一下。如果得到的字符串太长,还会进行截断处理。
原因分析:
用户执行导出后,后端返回的是一个包含了导出内容的oss地址,也就是一个Url。
前端直接把这个url放到<a>标签中。用户点击进行下载
下载时的交互
这种情况下,浏览器下载时展示在状态栏上的名字,浏览器就自由发挥了,目前浏览器的命名规则是将url上的非法字符去掉,然后拼一下。
下载的文件名
优化方案:
方案1:由服务器写入数据流的方式下载,同时由服务器指定一个自定义的文件名。
方案2:服务器返回存放业务数据的oss地址,前端指定一个自定义的文件名。
确定优化方案
最终选定了方案2。
原因是方案2改动最小,并且可以避免下载时导致业务数据缺失的问题。
客户都是用chrome,也规避了方案2的浏览器兼容性问题。
技术方案对比
方案1:
服务器返回数据流的方式
https://www.processon.com/view/6363c4d2e0b34d77dbcd4919
优点:
1. 可以由后端灵活自定义浏览器下载时的文件名。没有兼容性问题
2.代码实现简单。代码量少,实现简单
缺点:
1. 数据导出过程中如果出现异常,会出现只导出一部分数据的情况,整个下载过程并不会完全中断。用户没有办法分辨是否下载完成。
2. 服务器带宽打满后会影响其它功能的使用。服务器写数据到浏览器会占用服务器网卡的总带宽,如果打满,其它功能也用不了。可以把带宽想象成一座桥,大文件就像一个大卡车。
3. 影响到服务器的稳定性。大文件生成及传输过程会持续占用服务器内存。服务器的内存是有限的,下载大文件的功能占用了,其它功能就不能正常工作了。
4. 分布式环境中,增加了代码的复杂度。Feign或RestTempate在处理字节流时需要特殊的配置,在升级这些http客户组件时,也需要验证对这些已有功能的影响。
方案2:
使用oss作为数据中转站
https://www.processon.com/view/6363c4d2e0b34d77dbcd4919
优点:
1. 数据不会丢失。数据上传oss时报错时,整个导出就报错了,不会出现用户只拿到部分数据的情况
2. 不会占用服务器出口带宽。返回给前端的一个url,不管导出多大的文件,出口带宽都不会受到影响。
缺点:
1.兼容性问题。 在 HTML5 中,download 属性是 <a> 标签的新属性。
兼容性
定义和用法<a download="filename">filename 规定作为文件名来使用的文本。 该属性也可以设置一个值来规定下载文件的名称。所允许的值没有限制,浏览器将自动检测正确的文件扩展名并添加到文件 (.img,.xls,.doc,.pdf, .txt, .html, 等等)。 在 <a> 标签中必须设置 href 属性。 https://www.w3school.com.cn/tags/att_a_download.asp
2.需要下载的资源是同源的
同源策略(Same origin policy)是基于安全的考虑,是浏览器对JavaScript施加的安全限制,是浏览器最核心也最基本的安全功能。
简单地说只要在浏览器里打开页面,就默认遵守同源策略,目的是为了保证用户的隐私安全和数据安全。
那么什么是同源呢? 所谓同源是指两个 URL的"协议+域名+端口"三者都相同 https://www.runoob.com/tags/att-a-download.html
技术方案的详细设计
方案1:
按照http协议的要求,把业务数据转换成数据流写到HttpServletResponse
指定这个数据流的content-type:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
指定数据流的打开类型为attachment,文件名filename:
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
show me the code
https://github.com/helloworldtang/spring-boot-cookbook/blob/master/learning-demo/src/main/java/com/tangcheng/learning/adapter/openapi/ExportController.java
方案2:
show me the code
https://github.com/helloworldtang/spring-boot-cookbook/blob/master/learning-demo/src/main/resources/templates/jsDownload.html
技术方案小结:
导出场景,服务器直接返回数据流到前端,要关注占带宽的情况。
如果是由前端指定下载的下载名,需要考虑兼容性问题。
最佳方案:
服务器返回oss url,且指定自定义文件名。
注意事项:
要解决文件名相同时,并发操作相互覆盖的问题。
需要通过url对相同文件名的oss数据在path的维度进行区分。
比如url是这种格式 ip/userId/yyyy/MM/dd/hh/mm/ss/SSS/一个随机数/自定义文件名
小结:
用户接触到的地方都是需要精心设计的。
用户关注的地方就是有价值的地方,需要投入资源做好。