基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能


Posted in Vue.js onFebruary 23, 2021

1. 前言

之前公司要在管理系统中做一个全局上传插件,即切换各个页面的时候,上传界面还在并且上传不会受到影响,这在vue这种spa框架面前并不是什么难题。然而后端大佬说我们要实现分片上传、秒传以及断点续传的功能,听起来头都大了。

很久之前我写了一篇webuploader的文章,结果使用起来发现问题很多,且官方团队不再维护这个插件了, 经过多天调研及踩雷,最终决定基于vue-simple-uploader插件实现该功能,在项目中使用起来无痛且稳定。

如果你只是想实现基本的(非定制化的)上传功能,直接使用vue-simple-uploader,多读一下它的文档,不需要更多的二次封装。
如果你只是想实现全局上传插件,也可以参照一下我的实现。
如果你用到了分片上传、秒传及断点续传这些复杂的功能,恭喜你,这篇文章的重点就在于此。

本文源码在此:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploader

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

2. 关于vue-simple-uploader

vue-simple-uploader是基于 simple-uploader.js 封装的vue上传插件。它的优点包括且不限于以下几种:

  • 支持文件、多文件、文件夹上传;支持拖拽文件、文件夹上传
  • 可暂停、继续上传
  • 错误处理
  • 支持“秒传”,通过文件判断服务端是否已存在从而实现“秒传”
  • 分块上传
  • 支持进度、预估剩余时间、出错自动重试、重传等操作

读这篇文章之前,建议先读一遍simple-uploader.js的文档,然后再读一下vue-simple-uploader的文档,了解一下各个参数的作用是什么,我在这里假定大家已经比较熟悉了。。
vue-simple-uploader文档

simple-uploader.js文档

安装:npm install vue-simple-uploader --save
使用:在main.js中:

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

3. 基于vue-simple-uploader封装全局上传组件

引入vue-simple-uploader后,我们开始封装全局的上传组件globalUploader.vue,代码比较长,就不整个放出来了,源码放到github上了,这里一步一步地讲解。

template部分如下,本人自定义了模板和样式,所以html部分比较长,css部分暂时不列出,大家可以根据自己的ui去更改,主要关注一下uploader这个组件的options参数及文件addedsuccessprogresserror几个事件:

<template>
 <div id="global-uploader">

 <!-- 上传 -->
 <uploader
  ref="uploader"
  :options="options"
  :autoStart="false"
  @file-added="onFileAdded"
  @file-success="onFileSuccess"
  @file-progress="onFileProgress"
  @file-error="onFileError"
  class="uploader-app">
  <uploader-unsupport></uploader-unsupport>

  <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>

  <uploader-list v-show="panelShow">
  <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
   <div class="file-title">
   <h2>文件列表</h2>
   <div class="operate">
    <el-button @click="fileListShow" type="text" :title="collapse ? '展开':'折叠' ">
    <i class="iconfont" :class="collapse ? 'icon-fullscreen': 'icon-minus-round'"></i>
    </el-button>
    <el-button @click="close" type="text" title="关闭">
    <i class="iconfont icon-close"></i>
    </el-button>
   </div>
   </div>

   <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"><i class="nucfont inuc-empty-file"></i> 暂无待上传文件</div>
   </ul>
  </div>
  </uploader-list>

 </uploader>

 </div>
</template>

组件中的data部分:

data() {
 return {
 options: {
  target: 'http://xxxxx/xx', // 目标上传 URL
  chunkSize: '2048000', //分块大小
  fileParameterName: 'file', //上传文件时文件的参数名,默认file
  maxChunkRetries: 3, //最大自动失败重试上传次数
  testChunks: true, //是否开启服务器分片校验
  // 服务器分片校验函数,秒传及断点续传基础
  checkChunkUploadedByResponse: function (chunk, message) {
  let objMessage = JSON.parse(message);
  if (objMessage.skipUpload) {
   return true;
  }

  return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
  },
  headers: {
  // 在header中添加的验证,请根据实际业务来
  Authorization: "Bearer " + Ticket.get().access_token
  },
 },
 attrs: {
  // 接受的文件类型,形如['.png', '.jpg', '.jpeg', '.gif', '.bmp'...] 这里我封装了一下
  accept: ACCEPT_CONFIG.getAll()
 },
 panelShow: false, //选择文件后,展示上传panel
 }
},

全局引用:
app.vue中引用,即作为全局的组件一直存在,只不过在不使用的时候把上传界面隐藏了

<global-uploader></global-uploader>

4. 文件上传流程概览

1. 点击按钮,触发文件上传操作:

(如果你做的不是全局上传的功能,而是直接点击上传,忽略这一步。)

因为我做的是全局上传的插件,要先把上传的窗口隐藏起来,在点击某个上传按钮的时候,用Bus发送一个openUploader的事件,在globalUploader.vue中接收该事件,trigger我们uploader-btn的click事件。

在某个页面中,点击上传按钮,同时把要给后台的参数带过来(如果有的话),这里组件之间传值我用的event bus,当然用vuex会更好:

Bus.$emit('openUploader', {
 superiorID: this.superiorID
})

globalUploader.vue中接收该事件:

Bus.$on('openUploader', query => {
 this.params = query || {};

 if (this.$refs.uploadBtn) {
	 // 这样就打开了选择文件的操作窗口
 $('#global-uploader-btn').click();
 }
});

2. 选择文件后,将上传的窗口展示出来,开始md5的计算工作

onFileAdded(file) {
 this.panelShow = true;
	
	// 计算MD5,下文会提到
 this.computeMD5(file);
},

这里有个前提,我在uploader中将autoStart设为了false,为什么要这么做?

在选择文件之后,我要计算MD5,以此来实现断点续传及秒传的功能,所以选择文件后直接开始上传肯定不行,要等MD5计算完毕之后,再开始文件上传的操作。

具体的MD5计算方法,会在下面讲,这里先简单引出。

上传过程中,会不断触发file-progress上传进度的回调

// 文件进度的回调
onFileProgress(rootFile, file, chunk) {
 console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},

3. 文件上传成功后

文件上传成功后,在“上传完成”的回调中,通过服务端返回的needMerge字段,来判断是否需要再发送合并分片的请求,
如果这个字段为true,则需要给后台发一个请求合并的ajax请求,否则直接上传成功。

注意:这里的needMerge是我和后台商议决定的字段名

onFileSuccess(rootFile, file, response, chunk) {
 let res = JSON.parse(response);

 // 服务器自定义的错误,这种错误是Uploader无法拦截的
 if (!res.result) {
 this.$message({ message: res.message, type: 'error' });
 return
 }
	
	// 如果服务端返回需要合并
 if (res.needMerge) {
 api.mergeSimpleUpload({
  tempName: res.tempName,
  fileName: file.name,
  ...this.params,
 }).then(data => {
  // 文件合并成功
  Bus.$emit('fileSuccess', data);
 }).catch(e => {});
 // 不需要合并 
 } else {
 Bus.$emit('fileSuccess', res);
 console.log('上传成功');
 }
},

onFileError(rootFile, file, response, chunk) {
	console.log(error)
},

5. 文件分片

vue-simple-uploader自动将文件进行分片,在optionschunkSize中可以设置每个分片的大小。

如图:对于大文件来说,会发送多个请求,在设置testChunkstrue后(在插件中默认就是true),会发送与服务器进行分片校验的请求,下面的第一个get请求就是该请求;后面的每一个post请求都是上传分片的请求

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

看一下发送给服务端的参数,其中chunkNumber表示当前是第几个分片,totalChunks代表所有的分片数,这两个参数都是都是插件根据你设置的chunkSize来计算的。

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

需要注意的就是在最后文件上传成功的事件中,通过后台返回的字段,来判断是否要再给后台发送一个文件合并的请求。

6. MD5的计算过程

断点续传及秒传的基础是要计算文件的MD5,这是文件的唯一标识,然后服务器根据MD5进行判断,是进行秒传还是断点续传。

file-added事件之后,就计算MD5,我们最终的目的是将计算出来的MD5加到参数里传给后台,然后继续文件上传的操作,详细的思路步骤是:

  • 把uploader组件的autoStart设为false,即选择文件后不会自动开始上传
  • 先通过 file.pause()暂停文件,然后通过H5的FileReader接口读取文件
  • 将异步读取文件的结果进行MD5,这里我用的加密工具是spark-md5,你可以通过npm install spark-md5 --save来安装,也可以使用其他MD5加密工具。
  • file有个属性是uniqueIdentifier,代表文件唯一标示,我们把计算出来的MD5赋值给这个属性 file.uniqueIdentifier = md5,这就实现了我们最终的目的。
  • 通过file.resume()开始/继续文件上传。
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
 computeMD5(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 = 10 * 1024 * 1000;
 let chunks = Math.ceil(file.size / chunkSize);
 let spark = new SparkMD5.ArrayBuffer();
 
 // 文件状态设为"计算MD5"
 this.statusSet(file.id, 'md5');
 
 file.pause();
 
 loadNext();
 
 fileReader.onload = (e => {
 spark.append(e.target.result);
 if (currentChunk < chunks) {
  currentChunk++;
  loadNext();
  // 实时展示MD5的计算进度
  this.$nextTick(() => {
  $(`.myStatus_${file.id}`).text('校验MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%')
  })
 } else {
  let md5 = spark.end();
  this.computeMD5Success(md5, file);
  console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
 }
 });
 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.readAsArrayBuffer(blobSlice.call(file.file, start, end));
 }
},

computeMD5Success(md5, file) {
 // 将自定义参数直接加载uploader实例的opts上
 Object.assign(this.uploader.opts, {
 query: {
  ...this.params,
 }
 })
 file.uniqueIdentifier = md5;
 file.resume();
 this.statusRemove(file.id);
},

给file的uniqueIdentifier 属性赋值后,请求中的identifier即是我们计算出来的MD5

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

7. 秒传及断点续传

在计算完MD5后,我们就能谈断点续传及秒传的概念了。

服务器根据前端传过来的MD5去判断是否可以进行秒传或断点续传:

  • a. 服务器发现文件已经完全上传成功,则直接返回秒传的标识。
  • b. 服务器发现文件上传过分片信息,则返回这些分片信息,告诉前端继续上传,即断点续传。

7.1 对于前端来说

在每次上传过程的最开始,vue-simple-uploader会发送一个get请求,来问服务器我哪些分片已经上传过了,

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

这个请求返回的结果也有几种可能:

a. 如果是秒传,在请求结果中会有相应的标识,比如我这里是skipUploadtrue,且返回了url,代表服务器告诉我们这个文件已经有了,我直接把url给你,你不用再传了,这就是秒传。

图a1:秒传情况下后台返回值

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

图a2:秒传gif

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

b. 如果后台返回了分片信息,这是断点续传。如图,返回的数据中有个uploaded的字段,代表这些分片是已经上传过的了,插件会自动跳过这些分片的上传。

图b1:断点续传情况下后台返回值

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

图b2:断点续传gif

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

c. 可能什么都不会返回,那这就是个全新的文件了,走完整的分片上传逻辑

7.2 前端做分片检验:checkChunkUploadedByResponse

前面讲的是概念,现在说一说前端在拿到这些返回值之后怎么处理。
插件自己是不会判断哪个需要跳过的,在代码中由options中的checkChunkUploadedByResponse控制,它会根据 XHR 响应内容检测每个块是否上传成功了,成功的分片直接跳过上传
你要在这个函数中进行处理,可以跳过的情况下返回true即可。

checkChunkUploadedByResponse: function (chunk, message) {
	 let objMessage = JSON.parse(message);
 if (objMessage.skipUpload) {
  return true;
 }

 return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},

注:skipUploaduploaded 是我和后台商议的字段,你要按照后台实际返回的字段名来。

8. 源码及后记

总共几个文件,app.vue,封装的全局上传组件globalUploader.vue,调用组件的demo.vue,源码放到github上了:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploader。

globalUploader源码中的ticketapi都是自己用的, 一个是accesstoken,一个是基于axios封装的请求库,请根据你的业务需求替代之。另外上传界面的展开和收起用到了jquery,通知用到了Element的组件,请忽略之。

本人水平有限,更多的是提供一个思路,供大家参考。

封装完这个插件后,再加上开发文件资源库,我发现已经基本实现了一个简易的百度网盘了,一个管理系统,功能搞的这么复杂,坑爹啊!

8.1 关于第一个分片丢失问题

关于开启了testChunk后服务器收不到第一个分片的问题:
simpleUploader文档上是这么写的:

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

testChunk的那个get请求,默认带了第一个分片给服务端,如果服务端返回的是200状态,则假定当前块已经上传过了,不会再上传了;
所以这里服务器要改成其他http状态码,比如204,这样就不在“ 200, 201, 202”这个集合里了,代表服务端还没有这个块,需要按照标准模式上传,这样第一个分片就会再次被上传了

2019/8/6更新

1、优化了计算文件MD5的方式,展示MD5的计算进度

之前文章中计算MD5的方式为对整个文件直接计算MD5,很吃内存,容易导致浏览器崩溃
我改成了通过分片读取文件的方式计算MD5,防止直接读取大文件时因内存占用过大导致的网页卡顿、崩溃

2、新增了的自定义的状态

(之前我就封装了几种自定义状态,最近总有小伙伴问怎么没有“校验MD5”,“合并中”这些状态,我就把我的方法写出来了,方式很笨,但是能实现效果)

插件原本只支持了successerroruploadingpausedwaiting这几种状态,

由于业务需求,我额外增加了“校验MD5”“合并中”“转码中”“上传失败”这几种自定义的状态

由于前几种状态是插件已经封装好的,我不能改源码,只能用比较hack的方式:
当自定义状态开始时,要手动调一下statusSet方法,生成一个p标签盖在原本的状态上面;当自定义状态结束时,还要手动调用statusRemove移除该标签。

this.statusSet(file.id, 'merging');
this.statusRemove(file.id);

具体使用可以参考源码,同时希望simple-uploader的插件作者后面能够支持自定义状态的配置。

到此这篇关于基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能的文章就介绍到这了,更多相关vue simple uploader封装内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
全面解析Vue中的$nextTick
Dec 24 Vue.js
基于vue+echarts数据可视化大屏展示的实现
Dec 25 Vue.js
vue中封装axios并实现api接口的统一管理
Dec 25 Vue.js
vue-quill-editor插入图片路径太长问题解决方法
Jan 08 Vue.js
详解为什么Vue中的v-if和v-for不建议一起用
Jan 13 Vue.js
Vue 集成 PDF.js 实现 PDF 预览和添加水印的步骤
Jan 22 Vue.js
WebStorm无法正确识别Vue3组合式API的解决方案
Feb 18 Vue.js
vue中h5端打开app(判断是安卓还是苹果)
Feb 26 Vue.js
vue实现可移动的悬浮按钮
Mar 04 Vue.js
vue实现同时设置多个倒计时
May 20 Vue.js
vue中div禁止点击事件的实现
Apr 02 Vue.js
vue ant design 封装弹窗表单的使用
Jun 01 Vue.js
用vite搭建vue3应用的实现方法
Feb 22 #Vue.js
详解Vue3.0 + TypeScript + Vite初体验
Feb 22 #Vue.js
vue-cli 3如何使用vue-bootstrap-datetimepicker日期插件
Feb 20 #Vue.js
Vue实现todo应用的示例
Feb 20 #Vue.js
基于vue的video播放器的实现示例
Feb 19 #Vue.js
vue登录页实现使用cookie记住7天密码功能的方法
Feb 18 #Vue.js
Vue包大小优化的实现(从1.72M到94K)
Feb 18 #Vue.js
You might like
分页显示Oracle数据库记录的类之一
2006/10/09 PHP
NOD32 v2.70.32 简体中文封装版 提供下载了
2007/02/27 PHP
分享最受欢迎的5款PHP框架
2014/11/27 PHP
YII Framework框架教程之日志用法详解
2016/03/14 PHP
实例介绍PHP中zip_open()函数用法
2019/02/15 PHP
PHP抽象类与接口的区别详解
2019/03/21 PHP
对YUI扩展的Gird组件 Part-1
2007/03/10 Javascript
ExtJS的拖拽效果示例
2013/12/09 Javascript
jquery实现非叠加式的搜索框提示效果
2014/01/07 Javascript
js选项卡的实现方法
2015/02/09 Javascript
javascript类型系统——undefined和null全面了解
2016/07/13 Javascript
Angularjs根据json文件动态生成路由状态的实现方法
2017/04/17 Javascript
Vue.js框架路由使用方法实例详解
2017/08/25 Javascript
浅谈vue-router2路由参数注意的问题
2017/11/08 Javascript
JS对象与json字符串相互转换实现方法示例
2018/06/14 Javascript
详解为生产环境编译Angular2应用的方法
2018/12/10 Javascript
three.js利用卷积法如何实现物体描边效果
2019/11/27 Javascript
vue+vant使用图片预览功能ImagePreview的问题解决
2020/04/10 Javascript
在vue-cli3中使用axios获取本地json操作
2020/07/30 Javascript
[01:04:14]OG vs Winstrike 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
Python基础教程之正则表达式基本语法以及re模块
2016/03/25 Python
完美解决python遍历删除字典里值为空的元素报错问题
2016/09/11 Python
利用Python命令行传递实例化对象的方法
2016/11/02 Python
Tensorflow实现卷积神经网络的详细代码
2018/05/24 Python
python实现剪切功能
2019/01/23 Python
Python基于pyecharts实现关联图绘制
2020/03/27 Python
用python对excel进行操作(读,写,修改)
2020/12/25 Python
SOKOLOV官网:俄罗斯珠宝首饰品牌
2021/01/02 全球购物
沃尔玛旗下墨西哥超市:Bodega Aurrera
2020/11/13 全球购物
AJAX检测用户名是否存在的方法
2021/03/24 Javascript
红旗方阵解说词
2014/02/12 职场文书
个人租房协议书(范本)
2014/10/14 职场文书
解除劳动关系协议书2篇
2014/11/28 职场文书
中学生思想品德评语
2014/12/31 职场文书
大学生英文求职信范文
2015/03/19 职场文书
Vertica集成Apache Hudi重磅使用指南
2022/03/31 Servers