整体思路的第一步是结合项目背景,考察更优化的方案。文件上传失败是个老生常谈的问题。常见的解决方案是将一个大文件切分成多个小文件,请求接口并行上传。所有的请求都响应后,在服务器端合并所有的碎片文件。当分片上传失败时,重新上传时可以判断,只上传上次失败的部分,减少用户等待时间,减轻服务器压力。这是分段上传文件。如何分段上传大文件?流程图如下:大文件上传流程图分为以下几个步骤:1、文件MD5加密MD5是文件的唯一标识,可以通过文件的MD5查询文件的上传状态文件。根据文件的修改时间、文件名、最后修改时间等信息,通过spark-md5[2]生成文件的MD5。需要注意的是,大型文件需要分片读取,将读取的文件内容加入到spark-md5[3]的hash计算中,直到文件读取完成,最后将最终的hash码返回给callback回调函数中。这里可以根据需要添加文件读取的进度条。MD5加密过程的实现方法如下://修改时间+文件名+最后修改时间-->MD5md5File(file){returnnewPromise((resolve,reject)=>{letblobSlice=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSliceletchunkSize=file.size/100让chunks=100让currentChunk=0让spark=newSparkMD5.ArrayBuffer()让fileReader=newFileReader=加载文件。function(e){console.log('readchunknr',currentChunk+1,'of',chunks)spark.append(e.target.result)//添加数组缓冲区currentChunk++if(currentChunk=file.size?file.size:start+chunkSizefileReader.readAsArrayBuffer(blobSlice.call(file,nextstart,end))load}}2。查询文件状态前端获取到文件的MD5后,后台查看是否有名为MD5的文件夹。如果存在,则列出该文件夹下的所有文件,并获取上传的切片列表。如果不存在,则已上传。切片列表为空//MD5checkFileMD5(file,fileName,fileMd5Value,onError){constfileSize=file.sizeconst{chunkSize,uploadProgress}=thisthis.chunks=Math.ceil(fileSize/chunkSize)returnnewPromise(async(resolve,reject)=>{constparams={fileName:fileName,fileMd5Value:fileMd5Value,}const{ok,data}=awaitservices.checkFile(params)unk(okUp.chhaload){.lengthuploadProgress(file)resolve(data)}else{reject(ok)onError()}})}3.文件分片文件上传优化的核心是文件分片。Blob对象中的slice方法可以对文件进行切割,File对象继承了Blob对象,所以File对象也有slice方法。定义每个分片文件的size变量为chunkSize,通过文件大小FileSize和分片大小chunkSize得到分片chunk的个数,使用for循环和file.slice()方法对文件进行分片,序号为0-n,并比较上传的切片列表,得到所有未上传的切片,推送到请求列表requestList。文件碎片asynccheckAndUploadChunk(file,fileMd5Value,chunkList){let{chunks,upload}=thisconstrequestList=[]for(leti=0;i-1//如果已经存在,则无需上传当前区块if(!exit){requestList.push(upload(i,fileMd5Value,file))}}console.log({requestList})const结果=请求列表.length>0?Awaitpromise.all(requestlist).Then(result=>{console.log({result})returnresult.every}):trueconsole.log({result})returnresult===true}4.上传切片和调用Promise.all并发上传所有切片,并将切片编号、切片文件、文件MD5传给后台。后台收到上传请求后,首先检查名为文件MD5的文件夹是否存在,如果不存在则创建一个文件夹,然后使用fs-extra的rename方法将切片从临时路径移动到切片文件夹,结果如下:uploadFragmentation当所有分片上传成功后,会通知服务器合并。当有一个分片上传失败时,会提示“上传失败”。重新上传时,通过文件的MD5获取文件的上传状态。当服务器已经有该MD5对应的分片时,说明该分片已经上传,无需再次上传。当服务器找不到MD5对应的分片时,说明需要上传分片Slices。用户只需要上传这部分切片就可以上传整个文件。这是恢复的文件上传。续传示意图//uploadchunkupload(i,fileMd5Value,file){const{uploadProgress,chunks}=thisreturnnewPromise((resolve,reject)=>{let{chunkSize}=this//构造表单,FormData这是HTML5中的新功能letend=(i+1)*chunkSize>=file.size?file.size:(i+1)*chunkSizeletform=newFormData()form.append('data',file.slice(i*chunkSize,end))//file对象的slice方法用于切出文件的一部分form.append('total',chunks)//总切块数form.append('index',i)//Current是第一个Form.append('Filemd5value',FileMD5VALUE)Services.UPLOADLARGE(Form).then(data=>{if(data.ok){this.hasuploaded++upLoadprigu}.resolve(data)}).catch(err=>{reject(err)})})}5.上传进度分片批量上传虽然比大文件单次上传快很多,但是还是有一段时间加载。这时候要加入上传进度提醒,实时显示文件上传进度。NativeJavascript的XMLHttpRequest提供了一个progress事件,返回文件的上传大小和总大小。项目使用axios[4]封装ajax,可以在config中添加onUploadProgress方法来监控文件上传进度。上传进度constconfig={onUploadProgress:progressEvent=>{varcomplete=(progressEvent.loaded/progressEvent.total*100|0)+'%'}}services.uploadChunk(form,config)文件分片后,前面端主动通知服务器合并,服务器在收到请求时主动合并分片,通过文件MD5在服务器的文件上传路径中找到同名文件夹。从上面可以看出,文件分片是按照分片的序号命名的,分片上传接口是异步的,所以不能保证服务器收到的分片一定会按照请求的顺序进行拼接.因此,在合并文件夹中的碎片文件之前,先根据文件名排序,然后通过concat-files将碎片文件合并,得到用户上传的文件。至此,大文件上传完成。合并分片示意图节点代码://合并文件exports.merge={validate:{query:{fileName:Joi.string().trim().required().description('文件名'),md5:Joi.string().trim().required().description('filemd5'),size:Joi.string().trim().required().description('filesize'),},},权限:{roles:['user'],},异步处理程序(ctx){const{fileName,md5,size}=ctx.request.querylet{name,base:filename,ext}=path.parse(fileName)constnewFileName=randomFilename(name,ext)awaitmergeFiles(path.join(uploadDir,md5),uploadDir,newFileName,size).then(async()=>{constfile={key:newFileName,:nametypeme,.getType(`${uploadDir}/${newFileName}`),ext,path:`${uploadDir}/${newFileName}`,provider:'oss',size,owner:ctx.state.user.id,}}constkey=encodeURIComponent(file.key).replace(/%/g,'').slice(-100)file.url=awaituploadLocalFileToOss(file.path,key)ffile.url=confileUrl(file)stawaitFile.create(omit(file,'path'))constfiles=[]files.push(f)ctx.body=invokeMap(files,'toJSON')}).catch(()=>{throwBoom.badData('合并大文件失败,请稍后重试~')})},}总结上传多个分片,上传完成后通知服务器合并所有分片,实现分片上传大文件;原生XMLHttpRequest的onprogress监听切片的上传进度,实时获取文件的上传进度;spark-md5根据文件内容计算文件的MD5,得到文件Identifier的唯一性,与文件上传状态绑定;上传切片前通过文件MD5查询已上传切片列表,上传时只上传未上传的切片,实现断点续传