前言

对于大型文件的上传处理,我们不可能只是单纯的把整个文件,通过一个请求去上传,这样效率很低,且上传速度很慢。所以这种时候就需要前端去对上传的文件做分割处理,将文件分成一小片一小片的,然后再同时发起多个请求去上传这些片段,等文件上传完毕,最后再发起一个合并请求,在服务端上将这些片段合并,形成整个文件保存在服务器上,这就完成整个大型文件快速上传的过程。
那对于文件的分割处理等操作,HTML5 已经提供了一系列的 files API 给我们。但这里我们不去讲,该怎么去用这些 API 做分割上传文件的操作,而是介绍已经基于这些 API 封装的 simple-uploader 插件,一个能帮助我们快速的开发分片上传功能的插件。

插件介绍

文档资料:

  • simple-uploader API 文档
  • vue-simple-uploader 文档

摘自文档的第一句话:simple-uploader.js(也称 Uploader) 是一个上传库,支持多并发上传,文件夹、拖拽、可暂停继续、秒传、分块上传、出错自动重传、手工重传、进度、剩余时间、上传速度等特性;该上传库依赖 HTML5 File API。可见该插件已经帮我们封装好了很多功能,只需直接去用即可。
但接下来我细说的过程主要还是基于 vue-simple-uploader 这个插件去见,因为 vue-simple-uploader 插件是基于 simple-uploader 去封装的,所以它的 API 是和 simple-uploader 一模一样,只不过是封装成符合 vue 组件的形式去供我们使用而已。

过程思路

我会描述 前端后端 在整个分片上传中的大致过程,但具体代码只有前端的。
前端

  1. 在界面上传文件,得到 file 文件内容
  2. 对文件进行MD5处理,目的就是为了根据内容生成唯一的 key 标记 file,每个分片的文件片段都会有这个标记
  3. 发起检测(test)请求,服务端根据 key 标记查找判断上传了哪些片段,并返回已有片段在文件位置,有可直接跳过
  4. 组件内会对文件进行分割,每个被分割的片段都会执行 checkChunkUploadedByResponse
    这个函数,该函数的参数还有检查(test)请求的响应,根据响应的已上传片段位置数组去判断,例如:已上传[2,3],可文件总共分成四块,1和4未上传。函数返回
    true 代表已上传,返回 false 代表未上传。未上传的片段,插件会重新发起上传请求。
  5. 所有片段上传成功后,就会执行 fileSuccess
    函数,在该函数的回调里,可获得最后成功上传的请求响应结果,后端这时可返回一个字段,表示可以合并就发起合并请求。走到这步前端的工作就已经做完了。
  6. 暂停上传,插件已经提供暂停的功能,它会取消已经发起的但处于 pending
    状态的请求(还未响应的请求),这样就达到暂停效果,这里无需后端做操作。
  7. 取消上传,发起取消上传的请求,告诉后端这个文件不上传了,后端可以清理掉服务器上已上传的文件片段。

后端

  1. 检测(test)文件,根据某个 key 标记字段,检测文件在服务器上的上传情况,返回已上传部分位置给前端
  2. 保存文件分片,前端上传文件片段成功,得到文件片段并保存它的信息,根据 key 去划分这些文件片段的类,同 key 的片段是同文件。
  3. 合并分片,前后端两边都确认文件上传成功,得到前端发起的合并请求,就把同 key
    标记的文件片段根据它们的分块位置信息按顺序进行合并,得到完整的文件。
  4. 取消上传,若前端在上传一半时取消上传,还需对已上传的片段进行清理操作。
  5. 定时清理,因为某些文件上传过程中,可能会有其它意外导致文件上传失败或停止,前端也并未发起取消上传请求,可以设置一定时间(例如三天),在该时间内继续保存文件片段,超过这个时间也对这些文件片段进行清理操作。

代码

我所开发的项目是 vue2.0 ,以下代码仅做参考。
1、首先安装插件
安装好 vue-simple-uploader ,它会连带安装 simple-uploader。

npm install vue-simple-uploader --save

因为计算文件 MD5 的时候还需要用到 spark-md5 插件,所以也安装了这个插件

npm install spark-md5 --save

2、引入插件

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

3、使用
在引入 vue-simple-uploader 后,就会全局帮我们注册了 uploader、uploader-unsupport、uploader-btn 等等全局组件,这就是 vue 的开发思想,现成的组件轮子。
template 部分:

<uploaderref="myUploader":fileStatusText="fileStatusText":options="options":autoStart="false"@file-added="onFileAdded"@file-success="onFileSuccess"@file-error="onFileError"class="uploader-app"><uploader-unsupport></uploader-unsupport><!-- 这里我把选择上传文件按钮和拖拽组件结合在一起使用了--><uploader-btn ref="uploadBtn"><uploader-drop @click.native="()=>{$refs.uploadBtn.click}"><p>请点击虚线框选择要上传的文件或拖拽文件到虚线框内</p></uploader-drop></uploader-btn><uploader-list><div class="file-panel" slot-scope="props"><ul class="file-list"><li v-for="file in props.fileList" :key="file.id"><uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file></li><div class="no-file" v-if="!props.fileList.length">暂无待上传文件</div></ul></div></uploader-list>
</uploader>

js 部分:

export default {data() {const _this = thisreturn {// 用于替换组件原来的状态显示文字filsStatusText: {success:"成功",error:"失败",uploading:"上传中",paused:"暂停",waiting:"等待中"},// uploader 的主要配置options: {chunkSize: 2.5 * 1024 * 1024, // 允许片段最大为5M,因为最后一块默认是可以大于等于这个值,但必须小于它的两倍。simultaneousUploads: 5, // 最大并发上传数target:"xxx", // 目标上传 URL,若测试和上传接口不是同个路径,可以用函数模式permanentErrors:[404,415,500,501,409],// 原来默认是没有409的,但我这接口有409报错,需进入错误回调函数中提示错误信息// 每次发起测试校验,所有分片都会进入这个回调checkChunkUploadedByResponse:function (chunk,message) {// 每次校验,chunk(片段)会不一样,但message(只有一个测试请求)一样let objMessage = JSON.parse(message);// 后端认为这个文件上传过,则直接跳过if(objMessage.skipUpload) return true; // 若文件检测出现异常,则提示并返回falseif(objMessage.error) {_this.$message.error(objMessage.message);return false}// 一些校验信息,需要用到的,可以绑定在chunk上chunk.xxx = objMessage.xxx; // 可写可不写,看自己情况// chunk的offset就是分块后,该块片段在文件所有模块的位置return (objMessage.uploadedList||[]).indexOf(chunk.offset+1)>=0},// 处理所有请求的参数processParams:(params,file,chunk) => {// 这里需要根据后端的要求,处理一些请求参数params.xxx = chunk.xxx // 比如一些需要在上传时,带上测试校验返回的一些信息字段return params;}}}},methods:{// 导入文件时onFileAdded(file) {// 计算文件 MD5 并标记文件this.computeMD5(file);},// 上传失败onFileError(rootFile,file,res) {this.$message.error(JSON.parse(res).message);},// 取消文件上传onFileRemoved(file) {// 发起取消上传请求给后端this.$axios.cancelUploadFile({filename:file.name,identifier:file.uniqueIdentifer // 文件标记})},// 所有片段上传成功后,进入文件上传成功回调onFileSuccess(rootFile,file,res) {res = JSON.parse(res);// 后端返回成功的字段,插件认为只要所有片段的上传请求都成功了就是上传成功了,而对于其他的错误它是无法处理的if (!res.result) {this.$message.error(res.message);return}// 如果后端返回可以合并的字段则发起合并请求if(res.needMerge) {// 获取组件的成功状态显示dom节点const metaDom = document.querySelector(`.uploader-file.file_${file.id} .uploader-file-status`);// 因为插件在所有片段请求成功后就显示上传成功的状态// 可合并是否成功却不管了,而插件并未提供处理方式// 只能通过节点操作修改状态来处理了 metaDom.innerText = "合并中..."; this.$axios.mergeChunk({...}); // 发起合并请求} else {// 分片上传成功,但整个文件上传并未结束,不需要合并console.log("上传成功")}},// 根据文件内容计算 MD5 并标记文件 filecomputeMD5(file) {let fileReader = new FileReader();let time = new Date().getTime();let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;let currentChunk = 0;const chunkSize = this.options.chunkSize;let chunks = Math.ceil(file.size / chunkSize); // 总模块数let spark = new SparkMD5.ArrayBuffer();// 文件状态设为"计算MD5"this.statusSet(file.id, 'md5');file.pause();// 先暂停文件的上传过程loadNext(); // 开始读取文件内容// FileReader 加载完成fileReader.onload = (e => {// 插入读取的片段内容到 SparkMD5 中spark.append(e.target.result);// 按分片顺序读取,小于最后模块位置的就继续读取if (currentChunk < chunks) {currentChunk++;// 实时展示MD5的计算进度// 对于大型文件读取内容还是会花不少时间// 所以需要显示读取进度在界面let dom = document.querySelector(`.uploader-file.file_${file.id} .uploader-file-meta`);let md5Progress = ((currentChunk/chunks)*100).toFixed(0);if (md5Progress < 100) {dom.innerText = "MD5校验:"+md5Progress+"%"; }  else if(md5Progress === 100) {dom.innerText = "MD5校验完成"}loadNext();} else {// 所有文件分片内容读取完成,生成MD5let md5 = spark.end();// 在computeMD5Success中标记文件this.computeMD5Success(md5, file);console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);}});// FileReader 加载失败fileReader.onerror = function () {this.error(`文件${file.name}读取出错,请检查该文件`)file.cancel();};// 分片读取文件内容函数function loadNext() {let start = currentChunk * chunkSize;let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;// fileReader 读取文件fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));}},computeMD5Success(md5,file) {file.uniqueIdentifer = md5;// 标记文件filefile.resume();// 继续上传}}
}

分析和问题总结

分析

1、暂停功能
暂停功能无非就是前端取消了已发起但还在pending状态的请求和取消了剩下文件的继续上传(如果这里是要我们自己去写,那就要写很多了,幸好有插件帮我们实现了)
2、续传功能
暂停,刷新页面或在其它页面继续上传,续传功能主要还是依赖于 checkChunkUploadedByResponse 这个函数,其实每次重新上传前,都会执行文件校验,后端返回已接受到的文件片段位置,跳过这些已上传的文件片段达到续传的功能。
3、秒传
对于小文件实现的秒传功能,主要还是因为文件被分片上传了。比起以前的一个上传文件对一个接口,和现在的一个上传文件分片对多个接口,上传速度自然就不一样了,多个请求比一个请求实现上传快很多。
4、分块上传
分块(分片)的核心就是,文件根据内容,生成唯一的MD5标记,根据这个标记划分上传的文件,而分片后的offset即位置,就划分每个文件的片段(块),结合MD5的标记和它在文件中的位置,就可以让每一个片段都是唯一并且可识别的。
5、取消上传
这个几乎没变,取消上传清空组件该文件并请求该文件的清理接口,把该文件已上传的内容清理掉。

问题

1、合并的字段不是在最后上传的片段中返回
这个其实属于后端问题,因为上传文件是并发请求,按理来说,最后一个上传成功的片段,即最后一个响应的上传请求,应该带有需合并的字段才对。但问题是后端在倒数第二个片段就返回合并的字段,可插件并未进入上传成功的函数里,等最后一个也上传成功了,进入了成功回调,却又没有合并的字段。这就导致合并请求发不出去。
有人会说,既然进入插件成功的回调,那就直接合并不行吗?上面说过了,插件判定上传成功,是根据所有片段的上传请求都成功响应来定的。但若出现其它的错误,比如响应的状态码的确是请求成功的一个,可后端却没收到,即接口返回的上传 result 是 false 的,那很明显上传失败,更不应该请求合并。
那前端在请求响应的 processResponse 函数里操作呢? 就是在倒数第二个上传片段的请求响应中得到了可以合并的字段,那这样后端认为可合并,那就直接发起合并请求不行了吗?这个我也试过,但恰好出现虽然倒数第二个片段上传请求响应中出现可以合并的字段,但最后一个片段却上传失败的情况。
无论前端怎么改,到无法做到前后端统一认为可以合并的情况。所以前端还是必须使合并在成功的回调里,而不是写在其它地方。要改的是后端,后端需根据最大并发请求数量,去保证文件上传请求中最后一个片段才能带有可以合并的字段。

simple-uploader前端分片上传文件相关推荐

  1. 【转载】前端上传文件,python作为后端接收并保存到本地--Tornado上传文件--分片上传文件--更换pip下载源

    背景:在改造caffe自带demo时,增加了一个更新模型的功能,需要将用户训练好的caffemodel上传到服务器,并替换到已经存在的caffemodel文件,重新加载上传的caffemodel文件并 ...

  2. 上传文件慢,SpringBoot分片上传文件

    Java上传文件慢,大文件上传卡顿,请求超时怎么办? 话不多说直接上代码,代码复制过去可以直接使用 第一步:创建后端代码 package cn.leon.demo.rest;import lombok ...

  3. FastDfs分片上传文件实战

    1.引入的依赖 <!--redis依赖配置--> <dependency><groupId>org.springframework.boot</groupId ...

  4. ajax传图片以及后台接收,前端ajax上传文件,图片,后端nodejs接收文件;

    前端ajax上传文件,图片,后端nodejs接收文件: 学习了nodejs,就想实现一下有进度条的文件上传,html 在作这个功能的时候遇到的问题前端 用普通的ajax没法实现文件上传,只能post, ...

  5. Python3爬取迅捷语音转文字(包含持久化登陆和分片上传文件)

    前言 在这里我就不再一一介绍每个步骤的具体操作了,因为在上一次爬取今日头条数据的时候都已经讲的非常清楚了,所以在这里我只会在重点上讲述这个是这么实现的,如果想要看具体步骤请先去看我今日头条的文章内容, ...

  6. 前端表单七牛云php,记录一下前端分片上传七牛云踩过的坑

    起因 最近在工作中有个上传大文件的需求,原先咨询过组里的大佬给我推荐了百度的webupload,但后来引入之后发现它是基于jquery封装的.由于本身项目是基于vue开发的所以与jquery相关的开源 ...

  7. vue前端实现上传文件,vue 上传文件

    以ASP.NET Core WebAPI 作后端 API ,用 Vue 构建前端页面,用 Axios 从前端访问后端 API ,包括文件的上传和下载. 准备文件上传的API #region 文件上传  ...

  8. flask中使用FileField上传文件的两种方式+前端页面上传文件(flask三种上传文件方式)

    文章目录 上传文件方式一: 1.index.html文件: 2.主文件main.py: 上传文件方式二: 1.index2.html文件: 2.main.py文件: 上传文件方式三: 1.index3 ...

  9. vue前端实现上传文件的两种方式

    1.使用form表单的形式 第一种方式就是使用FormData的方式进行上传 html代码: <el-form :model="upform" :rules="up ...

最新文章

  1. python mysql类里_Python MySql 操作类
  2. python循环中append_[Python]list.append()在for循环中每次添加的都是最后的一个元素
  3. 微信小游戏复活了传统PC游戏
  4. 在latex或者mathtype中如何输入花体,如拉式量L
  5. linux不支持32,Visual Studio Code 1.36发布,不再支持Linux 32位
  6. uni-app + vue-cli3 安装axios、vant等依赖 - 操作篇
  7. 网页javascript部分
  8. 前端—每天5道面试题(十二)
  9. word标题大纲级别_word中级别设置 如何快速设置word大纲级别?
  10. 《黑客帝国 THE MATRIX》——当你生活在代码的虚拟世界中
  11. 站在两个世界的边缘——知无崖
  12. C++后台开发应该读的书
  13. xampp中MySQL启动错误问题
  14. 半阳不阳后的一些总结
  15. MySQL之虚拟列的详细讲解
  16. easyExcel中合并单元格文件读取实现方案
  17. 配置普通链接二维码规则,一直提示校验文件检查失败
  18. 【转载】经典10道c/c++语言经典笔试题(含全部所有参考答案)
  19. 全宇宙尺寸最小的OpenMV!OpenMV Mini!
  20. 王燕《应用时间序列分析》学习笔记2

热门文章

  1. 如何让你的blynk服务器随ubuntu系统启动?
  2. kylin的cube的原理
  3. HCIA学习笔记#1
  4. Opencv1.0视频处理与解码器
  5. 创业公司的薪酬激励设计
  6. 最美教师颁奖词计算机,微笑最美教师颁奖词2017
  7. 【转】16GB大内存该怎么玩儿?
  8. MFC双人版俄罗斯方块
  9. 华为mate30epro不支持鸿蒙,华为Mate30E Pro的“失败”或早已注定!
  10. w ndows7怎么一键恢复,windows7怎么一键还原?windows7恢复出厂设置教程