1.前期准备续传的原理是前端/服务器需要记住上传的分片,以便下次上传可以跳过之前上传的部分。实现记忆功能的方法有两种。前端使用localStorage记录上传切片的hash。服务器保存上传切片的哈希值。前端在每次上传前从服务器获取上传的分片。第一个是前端方案,第二个是服务器端,前端方案有个硬伤。如果换了浏览器就会失去记忆的效果,所以这里选择后者上传下载大文件需要一定的知识储备。其中融合了很多领域的知识点,很全面的一门技能,很好的融合了技能。写出这样的文章,也是很考验写作水平的。希望通过这篇文章,能够提高自己的理解,加深印象。说到文件,前端肯定有文件(File)、二进制(Blob)、文件读取(FileReader)。一次性上传大文件肯定不现实。需要对文件进行切分,后端采集后再进行整合。那么,Blob.prototype.slice或File.prototype.slice也是切片必不可少的。由于前端将资源分块,然后分别发送请求,也就是说一个文件对应一个上传请求,而现在可能变成一个文件对应n个上传请求(HTTP2的复用),所以前端可以基于Promise.all整合这多个接口,上传完成后,发送合并请求,通知服务端进行合并。合并时,可以使用nodejs中的读写流(readStream/writeStream),将所有分片的流通过管道输入到最终文件的流中。发送请求的资源时,前端会判断每个文件对应的序号(spark-md5),将当前区块、序号、文件哈希信息一起发送给服务器(因为计算哈希需要时间的内容,还需要考虑WebWorker),服务器合并时,可以按序号顺序合并。服务器端一旦上传请求失败,会返回当前区块失败的信息,包括文件名、文件哈希、区块大小、区块序号。这个时候把Promise.all换成Promise.allSettled是不是更方便。二、一系列相关知识点1、Blob、File和FileReader⑴。Blob:Blob对象表示一个不可变的、原始数据文件类对象。它的数据可以以文本或二进制格式读取,也可以转换为ReadableStream进行数据操作。Blob不一定代表JavaScript原生格式的数据。File接口基于Blob,继承了blob的功能并对其进行了扩展以支持用户系统上的文件。①要从其他非blob对象和数据构造Blob,需要使用Blob()构造函数:letblob=newBlob(array,options);②普通实例属性:属性/方法名读写说明Blob.prototype.sizeonly读取对象包含的数据大小(字节)Blob.prototype.type只读对象Blob包含数据的MIME类型.prototype.arrayBuffer()——返回一个包含blob中数据的Promise对象,并在ArrayBuffer中以二进制数据的形式表示,出现在FileReader.readAsArrayBuffer()返回的对象中。Blob.prototype.slice()–用于创建一个新的Blob对象,其中包含源blob指定字节范围内的数据,通常用于文件切片。⑵.File:通常,File对象来自用户在元素上选择文件后返回的FileList对象,或者来自拖放操作生成的DataTransfer对象,或者来自HTMLCanvas元素。File对象是Blob的一种特殊类型,可以在任何Blob类型的上下文中使用,Blob的方法可以被File使用。①File()构造函数创建一个新的File对象实例。letmyFile=newFile(bits,name[,options]);②普通实例属性:属性/方法名读写说明File.name只读返回当前File对象引用的文件名。File.size只读返回文件的大小。File.type只读返回文件的MIME类型File.slice([start[,end[,contentType]]])-返回一个新的Blob对象,该对象包含指定范围内的源Blob对象数据。如File.slice(currentIndex,currentIndex+size)⑶。FileReader:FileReader对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用File或Blob对象指定内容来读取文件或数据。File对象可以是用户在元素上选择文件后返回的FileList对象,也可以是拖拽操作生成的DataTransfer对象,也可以是HTMLCanvasElement执行mozGetAsFile()方法后返回的结果.①使用FileReader()构造函数创建一个新的FileReader。让阅读器=新文件阅读器();②常用实例属性:属性/方法名读写说明FileReader.readAsArrayBuffer()——开始读取指定Blob中的内容,一旦完成,result属性将保存到文件的ArrayBuffer数据对象,用于大数据,数百MB甚至数GB。FileReader.readAsText()——开始读取指定Blob中的内容。完成后,result属性会包含一个代表读取文件内容的字符串,可以用于小数据,几百KB,因为大文件会一次性加载到内存中,导致内存超出上限。FileReader.abort()-中止读取操作。返回时,readyState属性为DONE。⑷.URL:属性/方法名读写描述URL.createObjectURL()——创建一个DOMString,其中包含一个代表参数中给定对象的URL。此URL的生命周期与创建它的窗口中的文档相关联。这个新的URL对象代表指定的File对象或Blob对象。用于大文件下载。URL.revokeObjectURL()-用于释放通过调用URL.createObjectURL()创建的先前存在的URL对象。2.HTTP2的多路复用在HTTP/2中,有两个非常重要的概念,即frame和stream。帧代表最小的数据单元,每个帧标识该帧属于哪个流,流是由多个帧组成的数据流。HTTP2采用二进制数据帧传输,取代了HTTP1.x的文本格式,二进制格式解析更高效。多路复用取代了HTTP1.x的顺序和阻塞机制,所有对同一个域名的请求都通过同一个TCP连接并发完成。同一个Tcp可以发送多个请求,对端可以通过帧中的标识符知道属于哪个请求。通过该技术,可以避免旧版HTTP中的队头阻塞问题,传输性能得到大幅提升。3.大文件上传实现大文件上传需要前后端协同。下面主要说说前端需要实现的内容。前端部分有一个大概的思路。第一,一次性上传大文件导致线程挂掉或者速度太慢的问题。解决方案是拆分文件并同时上传。进而实现断点续传、文件秒传、暂停上传、续传等功能。这期间需要考虑性能优化。1、在前端贴上基本的html代码:upload
⑴如何进行分片处理然后合并发送?设置分片大小,有默认值,也可以传入参数。文件file是通过input的change事件获取的,然后通过file.slice切割成固定大小的片,每个片的内容slice通过spark-md5转换成对应的hash值,每次发送slice时携带对应的hash值+对应的值slice的下标作为唯一标识,使用promise发送ajax请求。全部。成功后向后台发送合并切片的请求,后台开始处理合并操作。①切割成固定大小的切片://生成文件切片+createFileChunk(file,size=SIZE){+constfileChunkList=[];+letcur=0;+while(cur{const{fileChunkList}=e.data;constspark=newself.SparkMD5.ArrayBuffer();让百分比=0;让计数=0;constloadNext=index=>{constreader=newFileReader();reader.readAsArrayBuffer(fileChunkList[index].file);reader.onload=e=>{count++;spark.append(e.target.result);if(count===fileChunkList.length){self.postMessage({percentage:100,hash:spark.end()});自我关闭();}else{百分比+=100/fileChunkList.length;self.postMessage({百分比});//递归计算loadNext(count);}};};loadNext(0);};②calculateHash函数计算哈希值:+calculateHash(fileChunkList){+returnnewPromise(resolve=>{+//添加worker属性+this.container.worker=newWorker("/hash.js");+this.container.worker.postMessage({fileChunkList});+this.container.worker.onmessage=e=>{+const{percentage,hash}=e.data;+this.hashPercentage=percentage;+if(hash){+resolve(hash);+}+};+});},③通过上传文件切片实现文件上传://uploadfileasynchandleUpload(){if(!this.container.file)return;constfileChunkList=this.createFileChunk(this.container.file);+this.container.hash=awaitthis.calculateHash(fileChunkList);this.data=fileChunkList.map(({file},index)=>({+fileHash:this.container.hash,chunk:file,hash:this.container.file.name+"-"+index}));等待this.uploadChunks();},//上传文件切片asyncuploadChunks(){constrequestList=this.data.map(({chunk,hash})=>{constformData=newF数据();formData.append("块",块);formData.append("散列",散列);formData.append("文件名",this.container.file.name);返回{表单数据};}).map(({formData})=>this.request({url:"http://localhost:3000",data:formData}));awaitPromise.all(requestList);+//合并切片+awaitthis.mergeRequest();//合并切片请求},④文件切片上传后发起合并切片请求://合并切片请求+asyncmergeRequest(){+awaitthis.request({+url:"http://localhost:3000/merge",+headers:{+"content-type":"application/json"+},+data:JSON.stringify({size:SIZE,//前端在请求时将之前设置的大小提供给服务器,服务器根据大小+文件名指定可读流的起始位置:this.container.file.name+})+});+}⑵如何实现文件秒传?续传的原理是前端/服务器需要记住上传的切片,以便下次上传可以跳过之前上传的部分。有两种方法可以实现记忆功能。前端可以使用localStorage记录上传的切片hash。服务器保存上传的分片哈希。前端在每次上传前从服务器获取上传的切片第一个是前端方案,第二个是服务器端,前端方案有缺陷。如果换了浏览器,就会失去记忆效果,所以这里选择后者其实根据前面的代码,文件会为每个分片生成不同的hash,所以只需要修改后端的接口即可。每次上传一个文件,先发送文件名hash,后端检测是否已经上传,然后后端保存服务器上存储的分片hash,对应的文件名,以及是否上传的标记已经完成。恢复上传请求发送后,后台检查相应的文件名是否已经完成。如果完成,会返回文件已经上传的信息,前端收到信息后会显示文件已经上传。其实这就是实现文件秒传的逻辑。它不是真正的上传,而是一种让你觉得它是真的上传的外观。如果没有完成,后端检测未上传的分片哈希,发送给前端。前端收到后,将hash对应的切片文件发送给后端,后端重复前面的步骤,最后合并切片,完成上传。⑶如何实现断点续传?断点续传顾名思义就是断点+续传,所以我们第一步要实现“断点”,即暂停上传。原理是利用XMLHttpRequest的abort方法取消发送一个xhr请求。为此,我们需要保存每个分片上传的xhr对象,然后修改请求方法。request({url,method="post",data,headers={},onProgress=e=>e,+requestList}){returnnewPromise(resolve=>{constxhr=newXMLHttpRequest();hr.load.xonprogress=onProgress;xhr.open(method,url);Object.keys(headers).forEach(key=>xhr.setRequestHeader(key,headers[key]));xhr.send(data);xhr.onload=e=>{+//从列表中删除请求成功的xhr+if(requestList){+constxhrIndex=requestList.findIndex(item=>item===xhr);+requestList.splice(xhrIndex,1);+}resolve({data:e.target.response});};+//对外暴露当前的xhr+requestList?.push(xhr);});requestList,所以requestList中只保存正在上传的切片的xhr。然后创建一个暂停按钮,当点击该按钮时,调用requestList中保存的xhr的abort方法,即取消清除所有正在上传的切片。handlePause(){this.requestList.forEach(xhr=>xhr?.abort());this.requestList=[];}想要恢复上传,必须知道是否需要上传,以及上传的切片,然后计算未上传的切片是否上传,上传成功的每个切片标记为已上传。如果所有切片都上传完毕,需要在后台调用mergeRequest方法将切片合并成一个完整的文件,然后返回前端提示文件上传成功。4.可以手把手下载大文件,带你完成下载大文件片段作为参考设计代码。主要思想和文件分片是一样的。所有切片下载完成后,点击链接(URL.createObjectURL,URL.revokeObjectURL)完成文件的下载合并。