根据部门的业务需求,需要在网络状态不良的情况下上传很大的文件(1G+)。
其中会遇到的问题:
1,文件过大,超出服务端的请求大小限制;
2,请求时间过长,请求超时;
3,传输中断,必须重新上传导致前功尽弃。
解决方案实现思路,拿到文件,保存文件唯一性标识,切割文件、分片上传、文件MD5验证、断点续传、手动重试上传。
鉴于过往有使用过webupload文件上传组件的经验,于是此次采用的是Plupload作为替换。Plupload是一款由著名的web编辑器TinyMCE团队开发的上传组件,简单易用且功能强大。
Plupload有以下功能和特点
- 拥有多种上传方式:HTML5、flash、silverlight以及传统的
<input type=”file” />
。Plupload会自动侦测当前的环境,选择最合适的上传方式,并且会优先使用HTML5的方式。所以你完全不用去操心当前的浏览器支持哪些上传方式,Plupload会自动为你选择最合适的方式。 - 支持以拖拽的方式来选取要上传的文件
- 支持在前端压缩图片,即在图片文件还未上传之前就对它进行压缩
- 可以直接读取原生的文件数据,这样的好处就是例如可以在图片文件还未上传之前就能把它显示在页面上预览
- 支持把大文件切割成小片进行上传,因为有些浏览器对很大的文件比如几G的一些文件无法上传。
环境
- vue2.x
- webpack3.x
- axios
代码
npm安装plupload,文件引入组件,
1 2 3 4 5 6 7 8 9 10 11 12 | <uploader browse_button= "upload_area" :max_retries= "3" :url= "action" :headers= "headers" chunk_size= "10MB" drop_element= "upload_area" @disableBrowse= "!loading" :BeforeUpload= "beforeUpload" :ChunkUploaded= "chunkUploaded" :FilesAdded= "filesAdded" :StateChanged= "stateChanged" @inputUploader= "inputUploader" /> |
初始化方法filesAdded(),每次上传前清空队列的其他文件,保证上传的一致性。其次对文件类型进行判断过滤fileType(),文件进入时进行总md5一次fileMd5(),然后进入文件分片chunkCheckStatus(),每个分片都要进行md5并与后台进行校验fileMd5(),确保文件在中断后继续上传的准确性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | filesAdded (up, files) { // 删除上传队列中其他文件,只保留最近上传的文件 let fileLen = files.length, that = this if (fileLen > 1) { files = files.splice(0, fileLen - 1) // 清空上传队列 } files.forEach((f) => { f.status = -1 that.dataForm.file = f that.fileType(f.getNative()) if (that.loading) { that.computeStatus = true that.progress = 0 // 文件分片 let chunkSize = 2097152, // Read in chunks of 2MB chunks = Math.ceil(f.size / chunkSize) that.fileMd5(f.getNative(), (e, md5) => { that.dataForm.md5 = md5 if (that.loading == true ) { that.count = 0 that.chunkCheckStatus(md5, that.dataForm.fileName, (uploader, dataList) => { that.uploading = uploader if (that.uploading == true ) { for ( let chunk = 1; chunk <= chunks; chunk++) { that.fileChunkFile(f.getNative(), chunk, (e, chunkFile) => { that.fileMd5(chunkFile, (e, blockMd5) => { that.PostFile(up, chunkFile, chunk, chunks, md5, blockMd5) }) }) } } else { // 去重 that.progress = 0 for ( let chunk = 1; chunk <= chunks; chunk++) { let status = 0 dataList.some((item) => { if (item.chunk == chunk) { status = 1 return false } }) if (status == 0) { that.fileChunkFile(f.getNative(), chunk, (e, chunkFile) => { that.fileMd5(chunkFile, (e, blockMd5) => { that.PostFile(up, chunkFile, chunk, chunks, md5, blockMd5) }) }) } } } }) } }) } }) } |
文件md5方法,这里使用了SparkMD5,import SparkMD5 from 'spark-md5'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | fileMd5 (file, callback) { let that = this var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, file = file, chunkSize = 2097152, // Read in chunks of 2MB chunks = Math.ceil(file.size / chunkSize), currentChunk = 0, spark = new SparkMD5.ArrayBuffer(), fileReader = new FileReader() fileReader.onload = function (e) { console.log( 'read chunk nr' , currentChunk + 1, 'of' , chunks) spark.append(e.target.result) // Append array buffer currentChunk++ if (currentChunk < chunks) { loadNext() } else { let blockMd5 = '' blockMd5 = spark.end() callback( null , blockMd5) } } fileReader.onerror = function () { callback( 'oops, something went wrong.' ) } function loadNext () { var start = currentChunk * chunkSize, end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) } loadNext() } |
文件分片上传方法,验证总分片信息后,把每个分片进行md5加密并上传校验,这里有写进度条相关的控制,不一一展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | chunkCheckStatus (md5, fileName, callback) { this .$http({ url: this .$http.adornUrl( '/biz/upload/getFileBlockStatus' ), method: 'get' , params: this .$http.adornParams({ md5: md5, fileName: fileName }) }).then(({ data }) => { if (data && data.code === 0) { if (data.list != null ) { this .uploading = false this .chunkCheckData = [] data.list.map((item, index) => { if (item.isUpload == true ) { this .count++ this .chunkCheckData.push(item) } }) callback( this .uploading, this .chunkCheckData) return } this .uploading = true callback( this .uploading) } else { this .$message.error(data.msg) this .loading = false this .computeStatus = false return false } }) } |
详细的配置信息可以参考我写的这篇文章:http://blog.ncmem.com/wordpress/2019/08/09/vue%e5%a4%a7%e6%96%87%e4%bb%b6%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/