vue-simple-uploader + spark-md5 大文件分片断点续传(本篇只做分片上传)

使用该技术解决的需求场景:
大文件上传响应超时。

解决方案:
将大文件切片,并计算 md5 传给后台,后台根据 md5 合并文件。

注意:
options 参数中 testChunks: false, // 默认true, 是否开启服务器分片校验
默认为 true,因为只做分片,不做断点续传,该配置需要为 false


参考资料:
https://github.com/simple-uploader/vue-uploader  vue-simple-uploader 的 github 地址
https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md vue-simple-uploader 的 github 地址(中文文档)
https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md simple-uploader.js 的 配置
https://www.cnblogs.com/xiahj/p/vue-simple-uploader.html 其实是参考了这篇的使用。这篇文章讲了断点续传,而我将该功能去掉了,只使用了分片

<template>
<div>

    <el-button type="primary" @click="uploadFile" >导入</el-button>

    <!-- 上传 -->
    <global-uploader ref="uploader" :tenantId="tenantId" :target="BIG_UPLOAD_TARGET" ></global-uploader>

</div>
</template>

<script>
import globalUploader from '@/components/common/globalUploader.vue'
components: {
    globalUploader
},
data() {
    return {
        BIG_UPLOAD_TARGET:  `${process.env.BASE_URL}/school/teacher/face/insertTeacherFaceByZip`,
    }
}
</script>

 

<!-- globalUploader.vue -->
<template>
    <div class="uploader_box">
        <div class="mask_box" v-show="panelShow"></div>
        <div id="global-uploader">

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

                <uploader-btn ref="uploadBtn" :single="true" v-show="false" :attrs="attrs" >选择文件</uploader-btn>

                <uploader-list v-show="panelShow">
                    <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
                        <div class="file-title">
                            <h2>文件列表</h2>
                        </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="iconfont icon-empty-file"></i> 暂无待上传文件</div>
                        </ul>
                    </div>
                </uploader-list>

            </uploader>

        </div>
    </div>
</template>

<script>
    /**
     *   全局上传插件
     */

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

    import SparkMD5 from 'spark-md5';

    const CHUNKSIZE = 1* 1024 * 1024;

    export default {
        props: {
            tenantId: String,
            target: String,
        },
        data() {
            return {
                options: {
                    target: this.target,
                    testChunks: false, // 默认true, 是否开启服务器分片校验, (为 true 时, 第一次请求该分片时不传 file 对象(此行为需后台配置处理,否则出错))
                    testMethod: 'POST',
                    // initialPaused: true, // 初始文件 paused 状态,默认 false。
                    chunkSize: CHUNKSIZE, // 分块时按照该值来分。最后一个上传块的大小是可能是大于等于1倍的这个值但是小于两倍的这个值大小,可见这个 Issue #51,默认 1*1024*1024。
                    forceChunkSize: true, // 是否强制所有的块都是小于等于 chunkSize 的值。默认是 false。
                    // fileParameterName: 'file',
                    maxChunkRetries: 3, // 最大自动失败重试上传次数,值可以是任意正整数,如果是 undefined 则代表无限次,默认 0。
                    // 服务器分片校验函数,秒传及断点续传基础
                    // checkChunkUploadedByResponse: function (chunk, message) { // 根据 XHR 响应内容检测每个块是否上传成功了
                    //     console.log('checkChunkUploadedByResponse', chunk, message);
                    //     let objMessage = JSON.parse(message);
                    //     return objMessage.data
                    // },
                    // preprocess: function (chunk) { // 每个块在测试以及上传前会被调用,参数就是当前上传块实例 Uploader.Chunk,注意在这个函数中你需要调用当前上传块实例的 preprocessFinished 方法,默认 null
                    //     console.log('preprocess', chunk);
                    //     chunk.preprocessFinished()
                    // },
                    headers: {
                        Authorization: JSON.parse(localStorage.getItem("MemberInfo")).Authorization,


                    },
                    // query: {
                    //     tenantId: this.tenantId
                    // }
                    // processParams: function (params) {}, // 处理请求参数,一般用于修改参数名字或者删除参数。0.5.2版本后,processParams 会有更多参数:(params, Uploader.File, Uploader.Chunk, isTest)。
                },
                
                statusText: {
                    success: '成功',
                    error: '出错了',
                    uploading: '上传中',
                    paused: '暂停中',
                    waiting: '等待中'
                },
                attrs: {
                    accept: ['.zip', '.rar']
                },
                panelShow: false,   //选择文件后,展示上传panel
                collapse: false,
            }
        },   
        mounted() {
            
        },
        computed: {
            //Uploader实例
            uploader() {
                return this.$refs.uploader.uploader;
            }
        },
        methods: {

            openUploader(){
                this.$refs.uploadBtn.$el.click()
            },

            onFileAdded(file) {
                this.panelShow = true;
                this.computeMD5(file);
            },
            onFileProgress(rootFile, file, chunk) {
                console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
            },
            onFileSuccess(rootFile, file, response, chunk) {
                console.log('onFileSuccess',rootFile, file, response, chunk);
                if(!response) return // 成功时没有返回值
                let res = JSON.parse(response);
                console.log('code', res.code);

                // 服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
                if (res.code!=200) { // 失败
                    this.$message({ message: res.message, type: 'error' });
                    return
                }else{
                    this.$message({ message: res.message, type: 'success' });
                    this.panelShow = false;
                }

                // 如果服务端返回需要合并
                // if (res.needMerge) {
                    // 文件状态设为“合并中”
                    // this.statusSet(file.id, 'merging');

                    // api.mergeSimpleUpload({
                    //     tempName: res.tempName,
                    //     fileName: file.name,
                    //     ...this.params,
                    // }).then(res => {
                    //     // 文件合并成功

                    // }).catch(e => {});

                // 不需要合并
                // } else {
                    // Bus.$emit('fileSuccess');
                    // console.log('上传成功');
                // }
            },
            
            onFileError(rootFile, file, response, chunk) {
                console.log('onFileError', rootFile, file, response, chunk);
                this.$message({
                    message: response,
                    type: 'error'
                })
            },

            /**
             * 计算md5,实现断点续传及秒传
             * @param file
             */
            computeMD5(file) {
                console.log('file', file);
                console.log('getRoot', file.getRoot());
                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 = CHUNKSIZE // 10 * 1024 * 1000;
                let chunks = Math.ceil(file.size / chunkSize); // 总块数
                let spark = new SparkMD5.ArrayBuffer();

                loadNext();

                fileReader.onload = (e => {
                    spark.append(e.target.result);

                    if (currentChunk < chunks) {
                        currentChunk++;
                        loadNext();
                    } 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.$notify({
                        title: '错误',
                        message: `文件${file.name}读取出错,请检查该文件`,
                        type: 'error',
                        duration: 2000
                    })
                    file.cancel();
                };

                function loadNext() {
                    console.log('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,
                        tenantId: this.tenantId
                    }
                })

                file.uniqueIdentifier = md5;
                file.resume();
            },

        },
        destroyed() {
        },
        components: {}
    }
</script>

<style lang="scss" scoped>
@import "@/scss/common.scss";
    .mask_box{
        position: fixed;
        z-index: 19; 
        left: 0;
        top: 0;
        width: 100vw;
        height: 100vh;
        background-color: rgba(0, 0, 0, 0.5);
    }
    #global-uploader {
        position: fixed;
        z-index: 20;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);

        .uploader-app {
            width: 520px;
        }

        .file-panel {
            background-color: #fff;
            border: 1px solid #e2e2e2;
            border-radius: 7px 7px 0 0;
            box-shadow: 0 0 10px rgba(0, 0, 0, .2);

            .file-title {
                display: flex;
                height: 40px;
                line-height: 40px;
                padding: 0 15px;
                border-bottom: 1px solid #ddd;

                .operate {
                    flex: 1;
                    text-align: right;
                }
            }

            .file-list {
                position: relative;
                height: 240px;
                overflow-x: hidden;
                overflow-y: auto;
                background-color: #fff;

                > li {
                    background-color: #fff;
                }
            }

            &.collapse {
                .file-title {
                    background-color: #E7ECF2;
                }
            }
        }

        .no-file {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 16px;
        }

        /deep/.uploader-file-icon {
            &:before {
                content: '' !important;
            }

            // &[icon=image] {
            //     background: url(./images/image-icon.png);
            // }
            // &[icon=video] {
            //     background: url(./images/video-icon.png);
            // }
            // &[icon=document] {
            //     background: url(./images/text-icon.png);
            // }
        }

        /deep/.uploader-file-actions > span {
            margin-right: 6px;
        }
    }

    /* 隐藏上传按钮 */
    #global-uploader-btn {
        position: absolute;
        clip: rect(0, 0, 0, 0);
    }
</style>


 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值