基于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 相关文章推荐
springboot+vue实现文件上传下载
Nov 17 Vue.js
vue实现图片裁剪后上传
Dec 16 Vue.js
vue 通过base64实现图片下载功能
Dec 19 Vue.js
vue 在服务器端直接修改请求的接口地址
Dec 19 Vue.js
Vue——前端生成二维码的示例
Dec 19 Vue.js
手写Vue源码之数据劫持示例详解
Jan 04 Vue.js
详解template标签用法(含vue中的用法总结)
Jan 12 Vue.js
vue使用vue-quill-editor富文本编辑器且将图片上传到服务器的功能
Jan 13 Vue.js
vue响应式原理与双向数据的深入解析
Jun 04 Vue.js
Vue全局事件总线你了解吗
Feb 24 Vue.js
Vue深入理解插槽slot的使用
Aug 05 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
PHP图片自动裁切应付不同尺寸的显示
2014/10/16 PHP
在Mac OS上编译安装Nginx+PHP+MariaDB开发环境的教程
2016/02/23 PHP
Laravel6.0.4中将添加计划任务事件的方法步骤
2019/10/15 PHP
在服务端(Page.Write)调用自定义的JS方法详解
2013/08/09 Javascript
Jquery实现图片左右自动滚动示例
2013/09/25 Javascript
JS读取XML文件示例代码
2013/11/15 Javascript
浅析JavaScript原型继承的陷阱
2013/12/03 Javascript
JavaScript日期时间与时间戳的转换函数分享
2015/01/31 Javascript
基于jQuery实现多层次的手风琴效果附源码
2015/09/21 Javascript
jquery获取url参数及url加参数的方法
2015/10/26 Javascript
AngularJS 实现弹性盒子布局的方法
2016/08/30 Javascript
微信小程序 动画的简单实例
2017/10/12 Javascript
vue init webpack myproject构建项目 ip不能访问的解决方法
2018/03/20 Javascript
详解vue2.6插槽更新v-slot用法总结
2019/03/09 Javascript
JavaScript动态检测密码强度原理及实现方法详解
2019/06/11 Javascript
jQuery实现全选按钮
2021/01/01 jQuery
[01:33]PWL开团时刻DAY2-开雾与反开雾
2020/10/31 DOTA
值得收藏的10道python 面试题
2019/04/15 Python
详解Python3 pandas.merge用法
2019/09/05 Python
Win 10下Anaconda虚拟环境的教程
2020/05/18 Python
全网最全python库selenium自动化使用详细教程
2021/01/12 Python
Luxplus瑞典:香水和美容护理折扣
2018/01/28 全球购物
新闻专业推荐信范文
2013/11/20 职场文书
运动会表扬稿大全
2014/01/16 职场文书
有关打架的检讨书
2014/01/25 职场文书
《雷鸣电闪波尔卡》教学反思
2014/02/23 职场文书
电脑售后服务承诺书
2014/03/27 职场文书
保护野生动物倡议书
2014/05/16 职场文书
政府四风问题整改措施
2014/10/04 职场文书
企业开业庆典答谢词
2015/01/20 职场文书
社区活动总结范文
2015/05/07 职场文书
2015年乡镇科普工作总结
2015/05/13 职场文书
2015企业年终工作总结范文
2015/05/27 职场文书
python基础之函数的定义和调用
2021/10/24 Python
MYSQL如何查看操作日志详解
2022/05/30 MySQL
Redis实现主从复制方式(Master&Slave)
2022/06/21 Redis