当前位置: 首页 > 科技观察

如何用JavaScript实现大文件的并行下载?

时间:2023-03-18 01:47:09 科技观察

本文转载自微信公众号《全栈修仙之路》,作者阿宝哥。转载本文请联系全栈修真之路公众号。JavaScript中如何实现并发控制?在这篇文章中,阿宝哥详细分析了async-pool库是如何使用Promise.all和Promise.race函数来实现异步任务的并发控制的。本篇阿宝哥将介绍如何使用async-pool库提供的asyncPool功能实现大文件的并行下载。上传大文件的解决方案相信有小伙伴已经了解了。在上传大文件时,为了提高上传效率,我们一般使用Blob。区块上传,所有区块上传成功后,再通知服务器合并区块。那么对于大文件的下载,是否可以采用类似的思路呢?在服务器支持Range请求头的情况下,我们也可以实现多线程分块下载的功能,如下图所示:看完上图相信你已经对下载的解决方案有了一定的了解大文件。下面先介绍一下HTTP范围请求。1.HTTP范围请求HTTP协议范围请求允许服务器只向客户端发送一部分HTTP消息。传输大型媒体文件或与恢复文件下载结合使用时,范围请求很有用。如果响应中存在Accept-Ranges标头(并且其值不是“none”),则表示服务器支持范围请求。在一个Range头中,一次可以请求多个部分,服务器会以多部分文件的形式返回。如果服务器返回范围响应,则应使用206PartialContent状态代码。如果请求的范围无效,服务器将返回416RangeNotSatisfiable状态码,表示客户端错误。允许服务器省略Range标头并返回带有200状态代码的整个文件。1.1范围语法Range:=-Range:=-Range:=-,-Range:=-,-,-unit:范围请求使用的单位,通常为字节。:整数,表示范围的起始值,单位为特定单位。:一个整数,表示特定单位范围的结束值。此值是可选的,如果不存在,则表示范围扩展到文档的末尾。了解了Range语法之后,我们来看一下实际使用示例:1.1.1Singlerange$curlhttp://i.imgur.com/z4d4kWk.jpg-i-H"Range:bytes=0-1023"1.1.2Multipleranges$curlhttp://www.example.com-i-H"Range:bytes=0-50,100-150"好了,HTTP范围请求的相关知识就先介绍到这里,我们进入正题,开始介绍如何实现大文件下载。2、如何下载大文件为了让大家更好的理解后面的内容,我们先来看一下整体的流程图:了解了下载大文件的过程之后,我们先来定义一下上述过程中涉及到的一些辅助函数。2.1定义辅助函数2.1.1定义getContentLength函数getContentLength函数顾名思义就是用来获取文件长度的。在这个函数中,我们通过发送一个HEAD请求,然后从响应头中读取Content-Length信息,得到当前url对应的文件的内容长度。functiongetContentLength(url){returnnewPromise((resolve,reject)=>{letxhr=newXMLHttpRequest();xhr.open("HEAD",url);xhr.send();xhr.onload=function(){resolve(~~xhr.getResponseHeader("Content-Length"));};xhr.onerror=reject;});}2.1.2定义asyncPool函数如何在JavaScript中实现并发控制?在本文中,我们介绍了asyncPool函数,它用于实现异步任务的并发控制。该函数接收3个参数:poolLimit(number类型):表示限制并发数;array(数组类型):表示任务数组;iteratorFn(函数类型):表示一个迭代函数,用于处理每个任务项,该函数返回一个Promise对象或一个异步函数。asyncfunctionasyncPool(poolLimit,array,iteratorFn){constret=[];//存储所有异步任务constexecuting=[];//存储正在执行的异步任务for(constitemofarray){//调用iteratorFn函数创建异步任务constp=Promise.resolve().then(()=>iteratorFn(item,array));ret.push(p);//保存新的异步任务//当poolLimit值小于等于任务总数时,执行并发控制if(poolLimit<=array.length){//当任务完成时,从正在执行的任务数组中取出完成的任务conste=p.then(()=>executing.splice(executing.indexOf(e),1));executing.push(e);//保存正在执行的异步任务if(executing.length>=poolLimit){awaitPromise.race(executing);//等待更快的任务执行完成}}}returnPromise.all(ret);}2.1.3定义getBinaryContent函数getBinaryContent函数用于根据传入的参数发起范围请求,从而下载文件da指定范围内的ta块:functiongetBinaryContent(url,start,end,i){returnnewPromise((resolve,reject)=>{try{letxhr=newXMLHttpRequest();xhr.open("GET",url,true);xhr.setRequestHeader("range",`bytes=${start}-${end}`);//在请求头上设置范围请求信息xhr.responseType="arraybuffer";//设置返回类型为arraybufferxhr.onload=function(){resolve({index:i,//文件块buffer的索引:xhr.response,//范围请求对对应的数据});};xhr.send();}catch(err){reject(newError(err));}});}需要注意的是ArrayBuffer对象是用来表示通用的,fixed-lengthrawbinaryDatabuffer我们不能直接操作ArrayBuffer的内容,而是通过类型数组对象或者DataView对象,将缓冲区中的数据表示为特定的格式,读写缓冲区的内容通过这些格式。2.1.4定义concatenate函数由于ArrayBuffer对象不能直接操作,我们需要先将ArrayBuffer对象转换为Uint8Array对象,然后再进行合并操作。下面定义的concatenate函数是将下载的文件数据块合并起来。具体代码如下:=0;for(letarrayofarrays){result.set(array,length);length+=array.length;}returnresult;}2.1.5定义saveAs函数saveAs函数用于实现客户端保存文件的功能,这里只是一个简单的实现。在实际项目中,可以考虑直接使用FileSaver.js。functionsaveAs({name,buffers,mime="application/octet-stream"}){constblob=newBlob([buffers],{type:mime});constblobUrl=URL.createObjectURL(blob);consta=document.createElement("a");a.download=name||Math.random();a.href=blobUrl;a.click();URL.revokeObjectURL(blob);}在saveAs函数中,我们使用了Blob和ObjectURL。其中ObjectURL是一个伪协议,允许Blob和File对象用作图像的URL源,下载二进制数据的链接等。在浏览器中,我们使用URL.createObjectURL方法创建一个ObjectURL。此方法接收一个Blob对象并以blob:/的形式为其创建一个唯一的URL。对应的例子如下:blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641浏览器内部为每个通过URL.createObjectURL生成的URL存储一个URL→Blob映射。因此,此类URL更短,但可以访问blob。生成的URL仅在当前文档打开时有效。好了,ObjectURL的相关内容就先介绍到这里。如果你想了解更多关于Blob和ObjectURL的知识,可以阅读你所不知道的Blob这篇文章。2.1.6定义下载函数下载函数用于实现下载操作,支持3个参数:url(字符串类型):预下载资源的地址;chunkSize(numbertype):chunk的大小,以字节为单位;poolLimit(number类型):表示限制并发数。asyncfunctiondownload({url,chunkSize,poolLimit=1}){constcontentLength=awaitgetContentLength(url);constchunks=typeofchunkSize==="number"?Math.ceil(contentLength/chunkSize):1;constresults=awaitasyncPool(poolLimit,[...newArray(chunks).keys()],(i)=>{letstart=i*chunkSize;letend=i+1==chunks?contentLength-1:(i+1)*chunkSize-1;returnBinaryContent(url,start,end,i);});constsortedBuffers=results.map((item)=>newUint8Array(item.buffer));returnconcatenate(sortedBuffers);}2.2基于大文件下载的例子在上面定义的辅助功能上,我们可以轻松地并行下载大文件。具体代码如下:-线程下载开始:"++newDate());down??load({url,chunkSize:0.1*1024*1024,poolLimit:6,}).then((buffers)=>{console.log("多线程下载ended:"++newDate());saveAs({buffers,name:"我的压缩包",mime:"application/zip"});});}由于完整的示例代码内容较多,阿宝哥具体代码就不贴了。感兴趣的小伙伴,可以访问以下地址浏览示例代码。完整示例代码:https://gist.github.com/semlinker/837211c039e6311e1e7629e5ee5f0a42下面我们看一下大文件下载示例的运行结果:3.总结本文介绍如何使用async-pool库提供的asyncPool在实现大文件并行下载的JavaScript函数。除了介绍asyncPool功能,阿宝哥还介绍了如何通过HEAD请求获取文件大小,如何发起HTTP范围请求,以及如何在客户端保存文件。其实使用asyncPool功能不仅可以实现大文件的并行下载,还可以实现大文件的并行上传。有兴趣的朋友可以自己试试。4.你所不知道的参考资料BlobMDN-ArrayBufferMDN-HTTP请求范围如何在JavaScript中实现并发控制?