基于vue-simple-uploader实现分片上传、秒传、断点续传的全局上传插件

7,943 阅读3分钟

前言

写这个组件之前,本来打算用webUploader的,结果官方不再维护了,而且在进行一系列的测试及老大的要求下,改用vue-simple-uploader,中间遇到了无数的问题,对于我这个脑子不好使的人来说,简直太难了。。。还好基本都解决了

1.关于vue-simple-uploader

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

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

在使用vue-simple-uploader之前,建议大家先看一下simple-uploader,里面有更详细的介绍,对于使用vue-simple-uploader有很大帮助

链接地址:simple-uploder.js文档

2.安装使用vue-simple-uploader

安装:npm install vue-simple-uploader --save

main.js\color{#ea4335}{main.js}中使用

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

3.封装全局上传组件

main.js中引入后,开始封装simpleUploader.vue\color{#ea4335}{simpleUploader.vue}(叫什么名字都可以)组件,在这里我自定义了上传组件的样式,但是我觉得应该符合大部分人的审美吧???

template部分如下:

<div class="global-uploader">
	<uploader
    ref="uploader"
    class="uploader-app"
    :options="options"
    :autoStart="false"
    @file-added="onFileAdded"
    @file-success="onFileSuccess"
    @file-error="onFileError">
    	<uploader-btn id="`uploadButton${getTime}`" class="uploader-btn">
        选择文件
        </uploader-btn>
        // 上传列表
        <uploader-list v-show="panelShow">
        	<div class="file-panel" slot-scope="props">
            	<div class="file-title">
                	<div style="display: flex;align-items: center; justify-content: space-between;">
                    	<h2>文件列表</h2>
                        // 总数量和已上传数量
                        <span>({{count}} / {{$refs.uploader ? $refs.uploader.fileList.length : 0}})</span>
                    </div>
                    <div class="operate">
                    	<el-button type="text" icon="el-icon-close" @click="close" title="关闭"></el-button>
                    </div>
                </div>
                <ul class="file-list">
                	<li v-for="file in props.fileList" :key="file.id">
                    	<uploader-file :file="file" :list="true"></uploader-file>
                    </li>
                    <div v-show="!props.fileList.length" class="no-file"></div>
                </ul>
            </div>
        </uploader-list>
    </uploader>
</div>

组件中的data部分:

export default {
	name: 'SimpleUploader',
    data () {
    	// 选择文件后显示
        panelShow: false,
        options: {
        	// 目标上传地址 写自己要上传到的地址即可
        	target: 'xxxxxx',
            // 分块大小
            chunkSize: '5242800',
            // 是否开启服务器分片校验
            testChunks: true,
            // 服务器分片校验函数 秒传及断点续传的基础
            checkChunkUploadedByResponse: (chunk, message) => {
            	let objMessage = JSON.parse(message);
                // 这里根据实际业务来 uploaded是后端返给我的字段 用来判断哪些片已经上传过了 不用再重复上传了
                return (objMessage.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
            }
        },
        getTime: (new Date().getTime()) / 1000
    }
}

可以封装成全局组件使用 也可在需要的地方引用即可

<simpleUploader ref="uploaderRef" @uploaderSuccess="uploaderSuccess"></simpleUploader>

4.上传流程预览

① 触发选择文件按钮

我这里是在使用组件的地方通过ref调用组件里面uploader-btn这个按钮的上传方法

this.$refs.uploaderRef.selectFile();

这里贴出selectFile这一上传方法的代码

 selectFile () {
 	// 这样就通过外边的方法触发组件里面上传按钮的上传方法
    // 这里按钮用了时间戳 是为了解决如果有同一页面用了好几个上传组件id不唯一问题
    // 如果页面中只用了一个上传组件 则忽略时间戳
 	document.getElementById(`uploadButton${this.getTime}`);
 }

这样就打开了选择文件的弹框 开始选择文件

② 选择文件后 打开上传列表弹框 开始计算MD5(文件唯一标识)

这里用到了 onFileAdded()\color{#ea4335}{onFileAdded()} 方法

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

这里有个前提,我在uploader中将autoStart\color{#ea4335}{autoStart}设为了false,为什么要这么做?

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

③ 文件上传成功后的回调

// 四个参数
onFileSuccess (rootFile, file, response, chunk) {
	// 这里我调了后端的接口 然后传参数(axios请求举例 按实际情况来)
    this.$axios({
         url: 'http://xxxx',
         method:'post',
         // params: params (参数-根据后端情况传参)
    }).then(res => {
        console.log(res);
    });
    // 成功后触发使用地方的方法 即uploaderSuccess
    this.$emit('uploaderSuccess');
}

5.文件分片

vue-simple-uploader自动将文件进行分片,在options的chunkSize中可以设置每个分片的大小。对于大文件来说,会发送多个请求,在设置testChunks为true后(在插件中默认就是true),会发送与服务器进行分片校验的请求 第一个就是该请求 下边的就是post上传文件请求

看一下参数 调接口时默认带的参数 不是自己传的

6.MD5的计算过程

断点续传及秒传的基础就是MD5的计算,是文件的唯一标识,然后服务器根据MD5判断是秒传还是断点续传 在onFileAdded这个方法中开始计算,然后放到参数里传给后端,具体的步骤

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()开始/继续文件上传。
上代码
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();
    
    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) {
	// 文件的唯一标识
    file.uniqueIdentifier = md5;
    file.resume();
},

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

7.上传失败方法

失败都会在onFileError这个方法里 和上传成功的方法一样返回四个参数,去做自己想要的错误提示即可

8.秒传及断点续传

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

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

** a. 服务器发现文件已经完全上传成功,则直接返回秒传的标识。**

b. 服务器发现文件上传过分片信息,则返回这些分片信息,告诉前端继续上传,即断点续传。

8.1 前端做分片校验

在代码中由options中的checkChunkUploadedByResponse控制,它会根据 XHR 响应内容检测每个块是否上传成功了,成功的分片直接跳过上传 你要在这个函数中进行处理,可以跳过的情况下返回true即可

checkChunkUploadedByResponse: (chunk, message) => {
    let objMessage = JSON.parse(message);
    // 这里根据实际业务来 uploaded是后端返给我的字段 用来判断哪些片已经上传过了 不用再重复上传了
    return (objMessage.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
}

这里我大概讲了一下流程 具体的还要在业务中具体分析,之后再一一完善