1. 前言

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

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

如果你只是想实现基本的(非定制化的)上传功能,直接使用vue-simple-uploader,多读一下它的文档,不需要更多的二次封装。

如果你只是想实现全局上传插件,也可以参照一下我的实现。

如果你用到了分片上传、秒传及断点续传这些复杂的功能,恭喜你,这篇文章的重点就在于此。

本文源码在此:https://github.com/shady-xia/Blog/tree/master/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请求都是上传分片的请求

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

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

6. MD5的计算过程

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

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

  1. 把uploader组件的autoStart设为false,即选择文件后不会自动开始上传
  2. 先通过 file.pause()暂停文件,然后通过H5的FileReader接口读取文件
  3. 将异步读取文件的结果进行MD5,这里我用的加密工具是spark-md5,你可以通过npm install spark-md5 --save来安装,也可以使用其他MD5加密工具。
  4. file有个属性是uniqueIdentifier,代表文件唯一标示,我们把计算出来的MD5赋值给这个属性 file.uniqueIdentifier = md5,这就实现了我们最终的目的。
  5. 通过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

7. 秒传及断点续传

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

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

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

7.1 对于前端来说

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

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

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

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

图a2:秒传gif

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

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

图b2:断点续传gif

  • 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文档上是这么写的:

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封装文件分片上传、秒传及断点续传的全局上传插件的更多相关文章

  1. Vue.js实现大文件分片md5断点续传

    背景 根据部门的业务需求,需要在网络状态不良的情况下上传很大的文件(1G+).其中会遇到的问题:1,文件过大,超出服务端的请求大小限制:2,请求时间过长,请求超时:3,传输中断,必须重新上传导致前功尽 ...

  2. 以寡治众各个击破,超大文件分片上传之构建基于Vue.js3.0+Ant-desgin+Tornado6纯异步IO高效写入服务

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_218 分治算法是一种很古老但很务实的方法.本意即使将一个较大的整体打碎分成小的局部,这样每个小的局部都不足以对抗大的整体.战国时期 ...

  3. 30分钟玩转Net MVC 基于WebUploader的大文件分片上传、断网续传、秒传(文末附带demo下载)

    现在的项目开发基本上都用到了上传文件功能,或图片,或文档,或视频.我们常用的常规上传已经能够满足当前要求了, 然而有时会出现如下问题: 文件过大(比如1G以上),超出服务端的请求大小限制: 请求时间过 ...

  4. vue+大文件分片上传

    最近公司在使用vue做工程项目,实现大文件分片上传. 网上找了一天,发现网上很多代码都存在很多问题,最后终于找到了一个符合要求的项目. 工程如下: 对项目的大文件上传功能做出分析,怎么实现大文件分片上 ...

  5. vue大文件分片上传插件

    最近遇见一个需要上传百兆大文件的需求,调研了七牛和腾讯云的切片分段上传功能,因此在此整理前端大文件上传相关功能的实现. 在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的Excel表 ...

  6. 聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_175 分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,在之前的一篇文章里:python花式读取大文件(10g/50 ...

  7. 基于springboot+jquery+H5的文件(并发+断点+分片)的上传方案

    1.支持文件分片断点续传 2.支持已上传文件再次上传时秒传 3.多个人对同一个文件同时上传可以多线程并发协调上传,加快超大文件的上传速度. 技术点:springboot + webflux + red ...

  8. Vue2.0结合webuploader实现文件分片上传

    Vue项目中遇到了大文件分片上传的问题,之前用过webuploader,索性就把Vue2.0与webuploader结合起来使用,封装了一个vue的上传组件,使用起来也比较舒爽. 上传就上传吧,为什么 ...

  9. Webuploader 大文件分片上传

    百度Webuploader 大文件分片上传(.net接收)   前阵子要做个大文件上传的功能,找来找去发现Webuploader还不错,关于她的介绍我就不再赘述. 动手前,在园子里找到了一篇不错的分片 ...

随机推荐

  1. 痞子衡嵌入式:语音处理工具Jays-PySPEECH诞生记(1)- 环境搭建(Python2.7.14 + PyAudio0.2.11 + Matplotlib2.2.3 + SpeechRecognition3.8.1 + pyttsx3 2.7)

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是语音处理工具Jays-PySPEECH诞生之环境搭建. 在写Jays-PySPEECH时需要先搭好开发环境,下表列出了开发过程中会用到的 ...

  2. Shell编程(week4_day1)--技术流ken

    本节内容 1.shell简介 2. shell分类 3. 查看shell 4. 第一个shell脚本 5. shell编程常用命令   5.1 grep   5.2 cut   5.3 sort   ...

  3. 【Angular专题】 (3)装饰器decorator,一块语法糖

    目录 一. Decorator装饰器 二. Typescript中的装饰器 2.1 类装饰器 2.2 方法装饰器 2.3 访问器装饰器 2.4 属性装饰器 2.5 参数装饰器 三. 用ES5代码模拟装 ...

  4. canvas API ,通俗的canvas基础知识(一)

    在没学canvas的时候,觉得canvas是这么的神秘,这么的绚丽,这么的高深,用canvas做出来的效果是如此的炫酷,能做的事情如此的宽广,简直让我心生敬畏之心,时常感叹:我要是得此技能,必定要上天 ...

  5. 如何使用apache的ab压力测试小工具传参数

    前言: windows下安装的phpstudy软件里集成的apache带了ab工具,所以可以不用单独下载.其他的操作系统下的安装或部署这里就不介绍了! 一. 使用windows的cmd进入apache ...

  6. 原生Js交互之DSBridge

    文章链接:https://mp.weixin.qq.com/s/Iqd0dKM-ZW4UwkIgSTnvYg 在上一篇文章「android 记一次富文本加载之路」中 介绍了关于android加载富文本 ...

  7. FIDDLER的使用方法及技巧总结

    转自: https://www.cnblogs.com/ink-marks/p/6363275.html 一.FIDDLER快速入门及使用场景 Fiddler的官方网站:http://www.fidd ...

  8. geth搭建私有网络

    geth --rpc --unlock "3ae88fe370c39384fc16da2c9e768cf5d2495b48,81063419f13cab5ac090cd8329d8fff9f ...

  9. python优雅编程之旅

    偶然的机会坐上了python的贼船,无奈只能一步步踏上王者之巅..... 参考博客地址:https://mp.weixin.qq.com/s/OZVT3iFrpFReqdYqVhUf6g 1.交换赋值 ...

  10. c/c++ 继承与多态 由子类向父类的转换规则

    问题1:子类B可以有3种方式(public, protected, private)继承父类A,用哪种方式继承,用户代码才能把子类B的对象转换成父类A的对象呢? 只用当子类B以public方式继承父类 ...