找回密码
 会员注册
查看: 19|回复: 0

前端如何高性能地上传大文件

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64756
发表于 2024-10-10 11:21:15 | 显示全部楼层 |阅读模式
前端如何高性能地上传大文件 前端如何高性能地上传大文件 郭雯宇@贝壳找房 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2020年08月27日 11:13 前端在处理文件上传时,通常一次性发送到server端,如果遇到大文件的时候,xhr请求会处理很长时间,这就大大增加了失败的概率,通常我们会将大文件切片然后发送。本文将循序渐进地聊聊,如何进行分片文件上传、文件分片的原理、如何解放js主进程。1. 文件分片通过input标签得到一个FileList(设置了multiple)或者File,其中每一个item(可以通过FileList.item(index)或者FileList[index])是一个File,而File继承于Blob,那么就可以使用Blob的方法来处理文件了。item的内容如下:看看文件各个字段含义:type: 文件的MIME type(这个属性可以用来做过滤和校验)size: 单位是字节name: 文件名使用slice方法可以将文件切成若干相等的块儿(最后一块儿可能小于其他块的大小,如:s3对分片要求最小的分片是5M,如果最后一块儿小于5M时需要合并到倒数第二块儿中),slice来自于file.__proto__。分片代码如下:constFILE_PER_PICE_SIZE=1024*1024*5;constsplitFile=(file,pieceSize)=>{letstart=0;letend;letindex=0;const{size=0}=file||{};if(pieceSizesize){end=size;}if(index===totalPieces-1){chucks.push({chuck:file.slice(start),index:index+1});break;}else{chucks.push({chuck:file.slice(start,end),index:index+1});start=end;index++;}}return{total:chucks.length,chucks:chucks.length===1[file]:chucks,};};2. 分片上传文件被分成若干块后,需要确保每一块儿都上传成功,也就是若干请求都成功,首先想到了Promise.all。constupload=(fileObj)=>{const{total,chucks,name}=fileObj;if(total){setLoading(true);consttype=(name&name.split(".").pop())||"";constreqList=[];getMultiKey(type||"video",name.replace(/\s/g,"")).then((res)=>{const{id,key}=res||{}romise.all(chucks.map((item,i)=>{const{chuck,index}=item||{};constformData=newFormData();formData.append("Body",chuck);formData.append("PartNumber",index);formData.append("Key",key);formData.append("UploadId",id);returnupload(formData,i,reqList);})).then((list)=>{checkUploadStatus({parts:list,id,key,}).then((url)=>{saveVideoInfo(url);setLoading(false);});}).catch(()=>{reqList.forEach((req)=>{req._xhr.abort();});message.destroy();message.error("上传失败,请重试!");cancelUpload({Key:key,UploadId:id});setLoading(false);});}).catch(()=>{setLoading(false);});}};请求过程中network面板如下:一次将所有的分片发出去,由于浏览器对同一个域名连接数量有限制(如:chrome是6个连接),这导致大量请求处于pending状态(也就是排队,hold在了浏览器,没有发出去),后面的请求可能因为排队而超时(超时的请求浏览会自动cancel了),只能将请求的超时时间设置的长一些(但是这个时间不好确定);而且还会阻塞了同域下的别的请求,这可能导致页面不能响应UI交互了。基于上述问题,不能一次将请求全部发出去,那么需要确定什么时候发请求并且需要知道文件什么时候能全部上传完毕。可以使用发布订阅模式,一次发出一定数量的分片,当收到响应后,再逐一发送剩余的分片。发布订阅模式有很多实际应用,这里不再赘述。我将使用Proxy来实现这一功能,代码如下(不涉及取消和重试的过程):constcreateUploaderProxy=(cb)=>{consttmp=Object.create(null);tmp.failed=[];//用来存储失败的分片,重试的时候使用tmp.done=[];//用来存储成功的分片标识tmp.pieceList=[];//用来存储待发送的分片tmp.multiConfig=Object.create(null);tmp.total=0;returnnewProxy({...tmp},{set(target,prop,value,receiver){if(prop==="done"||prop==="failed"){if(Array.isArray(value)&!value.length){target[prop]=value;returntrue;}target[prop].push(value);if(target.pieceList.length){constnext=target.pieceList.shift();uploader.singlePiece(next,target.multiConfig,receiver);returntrue;}if(target.failed.length+target.done.length===target.total){constfName=target.name;if(target.done.length===target.total){checkUploadStatus(target.done,target.multiConfig).then((url)=>{cb(url,true);//所有分片上传成功uploader.files[fName]=url;}).catch(()=>{cb(fName,false);//分片上传成功,但是获取文件在服务器上的地址失败});}else{cb(fName,false);//有分片上传失败}}returntrue;}target[prop]=value;returntrue;},});};constuploader=Object.create(null);uploader.files=Object.create(null);uploader.load=(file,config={accepts:["video/mp4","video/ogg","video/webm","video/quicktime"],pieceSize:1024*1024*5,waterFlow:6,},cb=()=>{})=>{let{name}=file;constfileUploader=createUploaderProxy(cb);uploader.files[name]={config,cb,uploader:fileUploader,};};uploader.singlePiece=(piece,fConfig,fileUploader)=>{upload(piece,fConfig).then((ret)=>{fileUploader.done=ret;}).catch(()=>{fileUploader.failed=piece;});}由上图可知,处于pending状态的请求数量一直是6个,不仅避免了排队超时的情况,同时还是释放浏览器资源。我们知道文件上传的过程中,不涉及页面的UI交互的,那么是不是可以在另一个单独的进程中处理呢?而web worker能够独立于主进程运行。3. web worker切记web worker不能访问dom和window,但是可以访问location,还有同源的限制。web worker的兼容性还不错,接下来将使用web worker来实现上述Proxy版本:/***worker.js*/constcreateUploaderProxy=()=>{consttmp=Object.create(null);tmp.failed=[];tmp.done=[];tmp.pieceList=[];tmp.multiConfig=Object.create(null);tmp.total=0;returnnewProxy({...tmp},{set(target,prop,value,receiver){if(prop==="done"||prop==="failed"){if(Array.isArray(value)&!value.length){target[prop]=value;returntrue;}target[prop].push(value);if(target.pieceList.length){constnext=target.pieceList.shift();uploader.singlePiece(next,target.multiConfig,receiver);returntrue;}if(target.failed.length+target.done.length===target.total){constfName=target.name;if(target.done.length===target.total){checkUploadStatus(target.done,target.multiConfig).then((url)=>{//成功的时候用postMessage通知主进程postMessage({url,success:true,});uploader.files[fName]=url;}).catch(()=>{//失败的时候用postMessage通知主进程postMessage({name:fName,success:false,});});}else{//失败的时候用postMessage通知主进程postMessage({name:fName,success:false,});}}returntrue;}target[prop]=value;returntrue;},});};constuploader=Object.create(null);uploader.files=Object.create(null);uploader.load=(file,config={accepts:["video/mp4","video/ogg","video/webm","video/quicktime"],pieceSize:1024*1024*5,waterFlow:6,})=>{let{name}=file;constfileUploader=createUploaderProxy();uploader.files[name]={config,cb,uploader:fileUploader,};};uploader.singlePiece=(piece,fConfig,fileUploader)=>{upload(piece,fConfig).then((ret)=>{fileUploader.done=ret;}).catch(()=>{fileUploader.failed=piece;});};onmessage=(event)=>{const{data:{isFile,file}={}}=event;uploader.load(file);};/***index.js*/if(window.Worker){//创建workerwindow.uploadWorker=newWorker("./worker.js");//监听worker响应结果window.uploadWorker.onmessage=(event)=>{console.log(event.data);};}//input的onchange事件constonChange=(event)=>{window.uploadWorker.postMessage({file,isFile:true});};使用worker后network面板如下:由上图可知,请求都是从worker中发出的,不占用主进程。由于worker有同源的限制,而我们为提高页面载入速度,一般会将静态资源放到cdn上,这样页面路径和静态资源路径就不同源了,所以需要将worker打包生成独立文件,并copy到服务器上。如果使用webpack打包,经过babel转换后带有浏览器的一些信息,这样就不能作为worker了,需要作为worker打包,推荐使用worker-plugin。//需要在index文件中增加如下代码/***index.js*/import"worker-plugin/loadername=upload!./uploadWorker.js";使用worker-plugin后,打包产物会增加:如果项目中含有nodejs,本地开发的时候需要能够访问到server中的worker,通过worker是放在client端,为方便调试,可以将client中的worker link到 server 端:lnclient/src/pages/videoUpload/uploadWorker.js/server/src/static/worker.js4. 总结由于大文件上传存在很多分片,这些分片如果一次性发出,会存在请求队列长时间排队的情况。如果网络环境不佳,队列中排队的请求有很大的概率因排队时间过长而超时。我们使用proxy的方式控制请求时机,从而避免这种超时情况,同时给予同域其他请求及时发出的时机。我们知道js是单线程,在该线程上要执行js、交互响应处理、异步请求、页面渲染等等工作,如果某一环节长期占用线程的话,就不能及时响应页面交互了,影响了用户体验。通过将文件分片、发送文件分片、处理响应、失败重发或取消这些操作放入webworker中执行,就能极大程度上减轻主线程的task量。5. 参考Filehttps://developer.mozilla.org/en-US/docs/Web/API/FileBlobhttps://developer.mozilla.org/en-US/docs/Web/API/BlobPromisehttps://es6.ruanyifeng.com/#docs/promiseproxyhttps://es6.ruanyifeng.com/#docs/proxyweb workerhttp://www.ruanyifeng.com/blog/2018/07/web-worker.htmlOff The Main Threadhttps://css-tricks.com/off-the-main-thread/worker-pluginhttps://github.com/GoogleChromeLabs/worker-plugin 预览时标签不可点 阅读原文关闭更多小程序广告搜索「undefined」网络结果 阅读原文
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-28 21:32 , Processed in 0.439490 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表