一、前端大文件上传的痛点
1. 文件过大导致带宽资源资源紧张,请求速度降低;
2. 可能导致后端进程卡死,请求无响应,前端页面卡顿,用户体验下降;
3. 甚至导致用户因页面卡顿而一直重复上传同一个大文件,页面崩溃;
4. 如果在上传过程中服务中断、网络中断、页面崩溃,可能文件要重新开始上传
二、痛点分析
首先分析一下上传流程:
前端选择文件后将文件上传,后端在处理文件过程中,首先会先将文件加载到运行内存中,之后再调用相应API进行写入硬盘内存的操作完成了整个文件的上传。
在一般小文件上传时这个流程不会出现什么问题,但是几个G的大文件,在这个过程中可能无法及时响应。
大文件上传过程中后端无法及时响应的原因:
1). 服务器的运行内存不可能无限大;
2). 文件越大导致运行内存紧张的几率越大,有可能造成运行内存的溢出;
3). 内存紧张导致服务器性能的损耗;
4).将大文件从运行内存整个写入到硬盘的过程也耗费时间与系统资源。
综上可知,对大文件的处理会影响服务器性能,某一个环节出问题会导致整个流程的雪崩,所以大文件直接上传是不可取的。
解决直接上传大文件造成服务器性能损耗及从中断处继续上传的方式是分片断点续传。
所谓断点续传,主要针对的情况是上传大文件(比如100M以上),如果文件较小,体现不出续传的优势。
三、断点续传原理
断点续传可以分为两部分: 断点和续传。
断点的概念:
在文件上传过程中将一个要上传的文件分成N块,然后使用多线程并发多块上传,因为某种原因上传被中断或暂停,此时中断/暂停的位置称为断点;
前端上传一片,将会被加载到运行内存中,加载完毕后再写入硬盘,此时运行内存中存储这个文件的临时变量被释放,然后此临时变量再被下一片占用,再进行写入,释放…,
续传的概念:
是指在从中断位置继续上传剩下的部分文件而不是从头开始进行上传。
当上传完毕之后,在服务器端进行合并(注意,合并的操作在后端进行,前端只需要请求接口,合并的方式由后端决定,是上传一片就合并一片,或者是上传所有的之后再整体进行合并)。
下面是断点续传的流程图:
1) 分片的实现
在html5出现之前,要想实现分块传输,只能用flash和activeX实现。
而HTML5 提供了文件二进制流进行分割的slice方法。
const chunks = Math.ceil(file.size / eachSize)
文件的分片,一般都在2-5M之间。这一步得到每片文件的内容、每块的序号、每块的大小、总块数等数据。
2) 续传的实现
1. 续传首先要确定需要继续上传的是哪一个文件,而确定一个文件的方式是对文件进行加密,只要某个文件内容发生任何变化,就需要重新上传。可以对文件进行MD5加密作为文件的唯一标识符,md5加密是不可逆的。要注意: 对整个大文件进行加密,可能会直接导致页面崩溃,需要对文件也进行分片加密,spark-md5插件支持文件的分片加密。代码部分可以参考一下资料:
spark-md5的加密
基于elementUI的spark-md5的使用
2. 在第一片文件上传之前,需要用文件名称+此文件唯一标识符+当前片数来查询文件是否上传过,如果上传过,上传到了第几片。通过服务器返回的已经上传的结果,我们可以通过分片结果获取到剩余的部分进行上传;如果没有上传过,便从第一块进行上传。如果是页面重新加载或者上传中断,只需要在重新上传之前查询在哪一片中断便可以继续上传。
3. 在上传完毕之后, 请求合并接口(合并接口也可以不请求,后端拿到所有文件片段之后自行进行合并),在服务器端将文件进行合并。此时整个过程就结束了。
4. 在上传的整个过程中,可以根据服务器端返回的当前上传成功的片数和总片数来对前端进度条进行展示。
四、知识点
参考资料:
1. 基于element的upload组件的分片上传
upload组件的分片上传
2. 关于上传的插件的使用:
在elementUI中的实现
在web-uploader中的实现