本文为:多文件断点续传、分片上传、秒传、重试机制 的更新版,若想看初始版本的实现,请查看该文章。

凡是要知其然知其所以然

文件上传相信很多朋友都有遇到过,那或许你也遇到过当上传大文件时,上传时间较长,且经常失败的困扰,并且失败后,又得重新上传很是烦人。那我们先了解下失败的原因吧!

据我了解大概有以下原因:

  1. 服务器配置:例如在PHP中默认的文件上传大小为8M【post_max_size = 8m】,若你在一个请求体中放入8M以上的内容时,便会出现异常
  2. 请求超时:当你设置了接口的超时时间为10s,那么上传大文件时,一个接口响应时间超过10s,那么便会被Faild掉。
  3. 网络波动:这个就属于不可控因素,也是较常见的问题。

基于以上原因,聪明的人们就想到了,将文件拆分多个小文件,依次上传,不就解决以上1,2问题嘛,这便是分片上传。 网络波动这个实在不可控,也许一阵大风刮来,就断网了呢。那这样好了,既然断网无法控制,那我可以控制只上传已经上传的文件内容,不就好了,这样大大加快了重新上传的速度。所以便有了“断点续传”一说。此时,人群中有人插了一嘴,有些文件我已经上传一遍了,为啥还要在上传,能不能不浪费我流量和时间。喔...这个嘛,简单,每次上传时判断下是否存在这个文件,若存在就不重新上传便可,于是又有了“秒传”一说。从此这"三兄弟" 便自行CP,统治了整个文件界。”


分片上传

HTML

原生INPUT样式较丑,这里通过样式叠加的方式,放一个Button.

  <div class="btns"><el-button-group><el-button :disabled="changeDisabled"><i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件<inputv-if="!changeDisabled"type="file":multiple="multiple"class="select-file-input":accept="accept"@change="handleFileChange"/></el-button><el-button :disabled="uploadDisabled" @click="handleUpload()"><i class="el-icon-upload el-icon--left" size="mini"></i>上传</el-button><el-button :disabled="pauseDisabled" @click="handlePause"><i class="el-icon-video-pause el-icon--left" size="mini"></i>暂停</el-button><el-button :disabled="resumeDisabled" @click="handleResume"><i class="el-icon-video-play el-icon--left" size="mini"></i>恢复</el-button><el-button :disabled="clearDisabled" @click="clearFiles"><i class="el-icon-video-play el-icon--left" size="mini"></i>清空</el-button></el-button-group><slot //data 数据var chunkSize = 10 * 1024 * 1024; // 切片大小
var fileIndex = 0; // 当前正在被遍历的文件下标data: () => ({container: {files: null},tempFilesArr: [], // 存储files信息cancels: [], // 存储要取消的请求tempThreads: 3,// 默认状态status: Status.wait}),

一个稍微好看的UI就出来了。

选择文件

选择文件过程中,需要对外暴露出几个钩子,熟悉elementUi的同学应该很眼熟,这几个钩子基本与其一致。onExceed:文件超出个数限制时的钩子、beforeUpload:文件上传之前

fileIndex 这个很重要,因为是多文件上传,所以定位当前正在被上传的文件就很重要,基本都靠它

handleFileChange(e) {const files = e.target.files;if (!files) return;Object.assign(this.$data, this.$options.data()); // 重置data所有数据fileIndex = 0; // 重置文件下标this.container.files = files;// 判断文件选择的个数if (this.limit && this.container.files.length > this.limit) {this.onExceed && this.onExceed(files);return;}// 因filelist不可编辑,故拷贝filelist 对象var index = 0; // 所选文件的下标,主要用于剔除文件后,原文件list与临时文件list不对应的情况for (const key in this.container.files) {if (this.container.files.hasOwnProperty(key)) {const file = this.container.files[key];if (this.beforeUpload) {const before = this.beforeUpload(file);if (before) {this.pushTempFile(file, index);}}if (!this.beforeUpload) {this.pushTempFile(file, index);}index++;}}
},
// 存入 tempFilesArr,为了上面的钩子,所以将代码做了拆分
pushTempFile(file, index) {// 额外的初始值const obj = {status: fileStatus.wait,chunkList: [],uploadProgress: 0,hashProgress: 0,index};for (const k in file) {obj[k] = file[k];}console.log('pushTempFile -> obj', obj);this.tempFilesArr.push(obj);
}

分片上传

  • 创建切片,循环分解文件即可
  createFileChunk(file, size = chunkSize) {const fileChunkList = [];var count = 0;while (count < file.size) {fileChunkList.push({file: file.slice(count, count + size)});count += size;}return fileChunkList;}

  • 循环创建切片,既然咱们做的是多文件,所以这里就有循环去处理,依次创建文件切片,及切片的上传。
async handleUpload(resume) {if (!this.container.files) return;this.status = Status.uploading;const filesArr = this.container.files;var tempFilesArr = this.tempFilesArr;for (let i = 0; i < tempFilesArr.length; i++) {fileIndex = i;//创建切片const fileChunkList = this.createFileChunk(filesArr[tempFilesArr[i].index]);tempFilesArr[i].fileHash ='xxxx'; // 先不用看这个,后面会讲,占个位置tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({fileHash: tempFilesArr[i].hash,fileName: tempFilesArr[i].name,index,hash: tempFilesArr[i].hash + '-' + index,chunk: file,size: file.size,uploaded: false,progress: 0, // 每个块的上传进度status: 'wait' // 上传状态,用作进度状态显示}));//上传切片await this.uploadChunks(this.tempFilesArr[i]);}
}

  • 上传切片,这个里需要考虑的问题较多,也算是核心吧,uploadChunks方法只负责构造传递给后端的数据,核心上传功能放到sendRequest方法中
 async uploadChunks(data) {var chunkData = data.chunkList;const requestDataList = chunkData.map(({ fileHash, chunk, fileName, index }) => {const formData = new FormData();formData.append('md5', fileHash);formData.append('file', chunk);formData.append('fileName', index); // 文件名使用切片的下标return { formData, index, fileName };});try {await this.sendRequest(requestDataList, chunkData);} catch (error) {// 上传有被reject的this.$message.error('亲 上传失败了,考虑重试下呦' + error);return;}// 合并切片const isUpload = chunkData.some(item => item.uploaded === false);console.log('created -> isUpload', isUpload);if (isUpload) {alert('存在失败的切片');} else {// 执行合并await this.mergeRequest(data);}
}

  • sendReques。上传这是最重要的地方,也是容易失败的地方,假设有10个分片,那我们若是直接发10个请求的话,很容易达到浏览器的瓶颈,所以需要对请求进行并发处理。

    • 并发处理:这里我使用for循环控制并发的初始并发数,然后在 handler 函数里调用自己,这样就控制了并发。在handler中,通过数组API.shift模拟队列的效果,来上传切片。
    • 重试: retryArr 数组存储每个切片文件请求的重试次数,做累加。比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次。为保证能与文件做对应,const index = formInfo.index; 我们直接从数据中拿之前定义好的index。 若失败后,将失败的请求重新加入队列即可。
    • 关于并发及重试我写了一个小Demo,若不理解可以自己在研究下,文件地址:https://github.com/pseudo-god/vue-simple-upload/blob/master/src/utils/sendRequest-domo.js , 重试代码好像被我弄丢了,大家要是有需求,我再补吧!
    // 并发处理
sendRequest(forms, chunkData) {var finished = 0;const total = forms.length;const that = this;const retryArr = []; // 数组存储每个文件hash请求的重试次数,做累加 比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次return new Promise((resolve, reject) => {const handler = () => {if (forms.length) {// 出栈const formInfo = forms.shift();const formData = formInfo.formData;const index = formInfo.index;instance.post('fileChunk', formData, {onUploadProgress: that.createProgresshandler(chunkData[index]),cancelToken: new CancelToken(c => this.cancels.push(c)),timeout: 0}).then(res => {console.log('handler -> res', res);// 更改状态chunkData[index].uploaded = true;chunkData[index].status = 'success';finished++;handler();}).catch(e => {// 若暂停,则禁止重试if (this.status === Status.pause) return;if (typeof retryArr[index] !== 'number') {retryArr[index] = 0;}// 更新状态chunkData[index].status = 'warning';// 累加错误次数retryArr[index]++;// 重试3次if (retryArr[index] >= this.chunkRetry) {return reject('重试失败', retryArr);}this.tempThreads++; // 释放当前占用的通道// 将失败的重新加入队列forms.push(formInfo);handler();});}if (finished >= total) {resolve('done');}};// 控制并发for (let i = 0; i < this.tempThreads; i++) {handler();}});
}

  • 切片的上传进度,通过axios的onUploadProgress事件,结合createProgresshandler方法进行维护
// 切片上传进度
createProgresshandler(item) {return p => {item.progress = parseInt(String((p.loaded / p.total) * 100));this.fileProgress();};
}

Hash计算

其实就是算一个文件的MD5值,MD5在整个项目中用到的地方也就几点。

  • 秒传,需要通过MD5值判断文件是否已存在。
  • 续传:需要用到MD5作为key值,当唯一值使用。

本项目主要使用worker处理,性能及速度都会有很大提升. 由于是多文件,所以HASH的计算进度也要体现在每个文件上,所以这里使用全局变量fileIndex来定位当前正在被上传的文件

// 生成文件 hash(web-worker)
calculateHash(fileChunkList) {return new Promise(resolve => {this.container.worker = new Worker('./hash.js');this.container.worker.postMessage({ fileChunkList });this.container.worker.onmessage = e => {const { percentage, hash } = e.data;if (this.tempFilesArr[fileIndex]) {this.tempFilesArr[fileIndex].hashProgress = Number(percentage.toFixed(0));}if (hash) {resolve(hash);}};});
}

因使用worker,所以我们不能直接使用NPM包方式使用MD5。需要单独去下载spark-md5.js文件,并引入

//hash.jsself.importScripts("/spark-md5.min.js"); // 导入脚本
// 生成文件 hash
self.onmessage = e => {const { fileChunkList } = e.data;const spark = new self.SparkMD5.ArrayBuffer();let percentage = 0;let count = 0;const loadNext = index => {const reader = new FileReader();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()});self.close();} else {percentage += 100 / fileChunkList.length;self.postMessage({percentage});loadNext(count);}};};loadNext(0);
};

文件合并

当我们的切片全部上传完毕后,就需要进行文件的合并,这里我们只需要请求接口即可

mergeRequest(data) {const obj = {md5: data.fileHash,fileName: data.name,fileChunkNum: data.chunkList.length};instance.post('fileChunk/merge', obj, {timeout: 0}).then((res) => {this.$message.success('上传成功');});}

Done: 至此一个分片上传的功能便已完成


断点续传

顾名思义,就是从那断的就从那开始,明确思路就很简单了。一般有2种方式,一种为服务器端返回,告知我从那开始,还有一种是浏览器端自行处理。2种方案各有优缺点。本项目使用第二种。
思路:已文件HASH为key值,每个切片上传成功后,记录下来便可。若需要续传时,直接跳过记录中已存在的便可。本项目将使用Localstorage进行存储,这里我已提前封装好addChunkStorage、getChunkStorage方法。
存储在Stroage的数据

在切片上传的axios成功回调中,存储已上传成功的切片

 instance.post('fileChunk', formData, ).then(res => {// 存储已上传的切片下标
+ this.addChunkStorage(chunkData[index].fileHash, index);handler();})

在切片上传前,先看下localstorage中是否存在已上传的切片,并修改uploaded

    async handleUpload(resume) {
+      const getChunkStorage = this.getChunkStorage(tempFilesArr[i].hash);tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
+        uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识:是否已完成上传
+        progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
+        status: getChunkStorage && getChunkStorage.includes(index)? 'success'
+              : 'wait' // 上传状态,用作进度状态显示}));}

构造切片数据时,过滤掉uploaded为true的

 async uploadChunks(data) {var chunkData = data.chunkList;const requestDataList = chunkData
+    .filter(({ uploaded }) => !uploaded).map(({ fileHash, chunk, fileName, index }) => {const formData = new FormData();formData.append('md5', fileHash);formData.append('file', chunk);formData.append('fileName', index); // 文件名使用切片的下标return { formData, index, fileName };})
}

Done: 续传到这里也就完成了。


秒传

这算是最简单的,只是听起来很厉害的样子。原理:计算整个文件的HASH,在执行上传操作前,向服务端发送请求,传递MD5值,后端进行文件检索。若服务器中已存在该文件,便不进行后续的任何操作,上传也便直接结束。大家一看就明白

async handleUpload(resume) {if (!this.container.files) return;const filesArr = this.container.files;var tempFilesArr = this.tempFilesArr;for (let i = 0; i < tempFilesArr.length; i++) {const fileChunkList = this.createFileChunk(filesArr[tempFilesArr[i].index]);// hash校验,是否为秒传
+      tempFilesArr[i].hash = await this.calculateHash(fileChunkList);
+      const verifyRes = await this.verifyUpload(
+        tempFilesArr[i].name,
+        tempFilesArr[i].hash
+      );
+      if (verifyRes.data.presence) {
+       tempFilesArr[i].status = fileStatus.secondPass;
+       tempFilesArr[i].uploadProgress = 100;
+      } else {console.log('开始上传切片文件----》', tempFilesArr[i].name);await this.uploadChunks(this.tempFilesArr[i]);}}}// 文件上传之前的校验: 校验文件是否已存在verifyUpload(fileName, fileHash) {return new Promise(resolve => {const obj = {md5: fileHash,fileName,...this.uploadArguments //传递其他参数};instance.post('fileChunk/presence', obj).then(res => {resolve(res.data);}).catch(err => {console.log('verifyUpload -> err', err);});});}

Done: 秒传到这里也就完成了。


后端处理

文章好像有点长了,具体代码逻辑就先不贴了,除非有人留言要求,嘻嘻,有时间再更新

Node版

请前往 https://github.com/pseudo-god/vue-simple-upload/blob/master/server/controller.js 查看

JAVA版

下周应该会更新处理

PHP版

1年多没写PHP了,抽空我会慢慢补上来

封装组件

写了一大堆,其实以上代码你直接复制也无法使用,这里我将此封装了一个组件。大家可以去github下载文件,里面有使用案例 ,若有用记得随手给个star,谢谢!
偷个懒,具体封装组件的代码就不列出来了,大家直接去下载文件查看,若有不明白的,可留言。

组件文档

Attribute

参数类型说明默认备注headersObject设置请求头before-uploadFunction上传文件前的钩子,返回false则停止上传acceptString接受上传的文件类型upload-argumentsObject上传文件时携带的参数with-credentialsBoolean是否传递CookiefalselimitNumber最大允许上传个数00为不限制on-exceedFunction文件超出个数限制时的钩子multipleBoolean是否为多选模式truebase-urlString由于本组件为内置的AXIOS,若你需要走代理,可以直接在这里配置你的基础路径chunk-sizeNumber每个切片的大小10MthreadsNumber请求的并发数3chunk-retryNumber错误重试次数3

Slot

方法名说明参数备注header按钮区域无tip提示说明文字无

后端接口文档:按文档实现即可

代码地址:https://github.com/pseudo-god/vue-simple-upload
接口文档地址 https://docs.apipost.cn/view/0e19f16d4470ed6b#287746

formdata上传文件_封装一个多文件断点续传、分片上传、秒传、重试机制的组件...相关推荐

  1. java zip 替换文件_替换一个Zip文件而不解压缩在Java

    我有一个zip文件,我想用另一个文件替换它里面的一个文件.因此,不需要删除zip条目,只需将zip条目的文件替换为另一个条目即可.替换一个Zip文件而不解压缩在Java 这是我试过的. public ...

  2. python下载bt文件_给定一个.torrent文件,如何在python中生成一个磁力链接?

    小编典典 您可以使用从BitTorrent中提取的bencode模块来执行此操作. 为了展示一个例子,我从这里下载了Ubuntu的torrent ISO: http://releases.ubuntu ...

  3. 宏文件下载_用一个宏文件,就一个轻松把SolidWorks的英文特征翻译成中文

    以前分享过一些国外的模型,左侧设计树都是英文,许多初学者看着很不方便.最近在网上发现一个宏程序,可以直接把标准的英文特征翻译成中文(修改过特征名称的不行). 今天把这个文件分享出来,希望对大家有所帮助 ...

  4. 怎么将几张pdf合并成一张_如何将多个pdf文件合并成一个pdf文件?

    原标题:如何将多个pdf文件合并成一个pdf文件? 我很喜欢使用PDF文件格式,为什么呢?因为PDF具有许多其他电子文档格式无法相比的优点.PDF文件格式可以将文字.字型.格式.颜色及独立于设备和分辨 ...

  5. 如何在另一个JavaScript文件中包含一个JavaScript文件?

    JavaScript中是否有类似于CSS中@import的内容,可让您在另一个JavaScript文件中包含一个JavaScript文件? #1楼 而不是在运行时添加,而是使用脚本在上传之前进行串联. ...

  6. ncl 多个单一时间文件合并成一个nc文件_iOS逆向--MachoO文件

    作者:海浪宝宝 链接:https://juejin.im/post/6886083253262876685 我们先创建一个目录:cd到该目录中,然后通过vi命令创建一个.c文件 vi test.c 在 ...

  7. 如何将大量图片文件合并成一个*.bin文件

    文章目录 1.bin是啥 2. 首先将大量图片全都导成按你所要顺序编号的.bin文件 3.将这些.bin文件合成为一个.bin文件 4.问题它出现了(如果你没有出现问题此后内容可不看) 5.新的尝试 ...

  8. less (三) 一个less 文件引用另一个less文件

    现在假设我们写了两个less文件,一个文件是a.less  另一个文件是 b.less; 那么a.less文件中如何引用b.less中的文件呢? 例子: 先新建如下的目录结构 文件信息: index. ...

  9. 如何将多个PDF文件合并为一个PDF文件?PDF文件合并教程

    如何合并PDF文件?PDF我们都不陌生,很多人经常在网站上搜狐各种类型的PDF资料,但是有时因为资料太多不好管理和查阅,如果可以把同类型的多个PDF文件合并为一个PDF文件,那无论是保存还是查阅,都会 ...

最新文章

  1. centos7上开启单用户模式
  2. Filemanager 的使用
  3. Spring中的注解@Service @Component @Controller @Repository区别
  4. android 六边形简书,深入理解六边形架构
  5. 【Python小游戏】当当当当 万众瞩目得《滑雪大冒险》来啦~(附源码)
  6. JEECG 命名规范
  7. CCF推荐国际学术会议与学术期刊
  8. 15种微信小程序运营推广方法
  9. c 自动打印的服务器,C-Lodop云打印服务器 x64
  10. BZOJ 2037: [Sdoi2008]Sue的小球(DP)
  11. XSS漏洞利用---PHPMyWind 任意密码重置漏洞
  12. h5与原生app交互的原理
  13. 开源项目推荐:NS游戏模拟器Ryujinx
  14. C语言入门(八)一维数组
  15. 2021年中国皮革行业现状分析:销售收入同比增长8.4%[图]
  16. 总线概述及常见总线(转)
  17. matlab 光栅 傅里叶,光栅原理及MATLAB仿真汇编.doc
  18. 源于日本的工作之道“守破离”是个学习路径的好方法
  19. 华为 GT Runner测评:为跑者而生的专业跑表
  20. layui下拉选择框开启搜索功能后,文本框会将Nbsp显示出来的解决办法

热门文章

  1. Binary classification - 聊聊评价指标的那些事儿【回忆篇】
  2. LeetCode Encode and Decode TinyURL
  3. mysql创建视图不允许子查询
  4. 关于Bootstrap的理解
  5. WPF and Silverlight 学习笔记(十九):WPF更换主题
  6. asp Upload
  7. windows的libcurl下zlib1.dll问题
  8. STM32工作笔记0049---JLINK在线调试__软件调试方法与技巧
  9. warpctc error
  10. 根据sessionId获取Session对象