记录第一次实现大文件切片上传及相关问题

大文件切片上传

<div class="t-upload">
    <el-upload
        ref="upload"
        :action="action"
        :accept="accept"
        :multiple="multiple"
        :headers="headers"
        :show-file-list="needFileList"
        :file-list="fileList"
        :on-change="onChange"
        :auto-upload="false"
        :on-remove="onRemove"
        :on-success="onSuccess"
        :on-error="onError"
        v-bind="[$props, $attrs]"
        v-on="$listeners"
    >
        <div
            slot="trigger"
            class="t-upload-trigger"
        >
            <template v-if="!$slots.trigger">
                <el-button :disabled="disabled">
                    <a-svg-icon
                        class="t-upload-button-icon"
                        name="upload-file"
                    />
                    <span
                        v-if="!hasFile"
                        class="t-upload-button-text"
                    >
                        {{ buttonText }}
                    </span>
                    <span
                        v-else
                        class="t-upload-button-text"
                    >
                        {{ reUploadButtonText }}
                    </span>
                </el-button>
            </template>
            <slot
                v-else
                name="trigger"
            >
            </slot>
        </div>
        <div
            v-if="tips.length"
            slot="tip"
            class="t-upload-tip"
        >
            <ul>
                <li
                    v-for="(item, index) in tips"
                    :key="index"
                    v-dompurify-html="(index > 0 ? `${index}、` : '') + item"
                    class="el-upload__tip"
                >
                </li>
            </ul>
        </div>
    </el-upload>
</div>
import apis from '@src/common/api';
import {request} from '@src/common/utils';
import {getAuthorization} from '@src/common/utils/helper';
import SparkMD5 from 'spark-md5';
const splitSize = window.$AICP.splitSize || 5; // 分片大小单位M
const limitPool = 3; // 最大并发
const chunkSize = splitSize * 1024 * 1024;// 定义分片的大小 方便测试

export default {
    name: 'SplitUpload',
    props: {
        action: {
            type: String,
            default: apis.url.fileUpload
        },
        accept: {
            type: String,
            default: ''
        },
        multiple: {
            type: Boolean,
            default: false
        },
        maxSize: {
            type: Number,
            default: 200
        },
        buttonText: {
            type: String,
            default: '点击上传'
        },
        reUploadButtonText: {
            type: String,
            default: '重新上传'
        },
        tips: {
            type: Array,
            default: () => ([])
        },
        successTip: {
            type: String,
            default: '上传成功!'
        },
        errorTip: {
            type: String,
            default: '上传失败!'
        },
        // 是否已上传文件
        uploadedFile: {
            type: Boolean,
            default: false
        },
        fileNameList: {
            type: Array,
            default: () => ([])
        },
        // 是否需要展示上传的文件列表
        needFileList: {
            type: Boolean,
            default: true
        },
        /* eslint-disable */
        validate: {
            type: Function
        },
        successCallback: {
            type: Function
        },
        errorCallback: {
            type: Function
        },
        isSuper: {
            type: Boolean,
            default: false
        }
        /* eslint-enable */
    },
    data() {
        return {
            hasFile: false,
            disabled: false,
            /*
             * fileList是只读的直接修改fileList会报错,无需在beforeUpload和onchange中push你上传的文件,elUpload也能拿到对应的
             * fileList,因为他的fileList是在on-start 钩子中拿的 具体可看源码-element-dev/packages/upload/src/index.vue
             * */
            fileList: this.fileNameList || [],
            hasUploadChunk: []
        };
    },
    computed: {
        headers() {
            return {
                'Authorization': this.isSuper ? '' : getAuthorization(),
            };
        }
    },
    mounted() {
        if (this.uploadedFile) {
            this.hasFile = true;
        }
    },
    methods: {
        async onChange(File, fileList) {
            if (typeof this.validate === 'function') {
                return this.validate(File);
            }
            if (this.accept) {
                let exts = this.accept.split(',');
                let ext = File.name.substring(File.name.lastIndexOf('.')).toLowerCase();
                if (!exts.includes(ext)) {
                    // 上传失败 将上传失败的文件信息删除 否则计时上传失败 一样会展示上传失败的文件名
                    this.$refs.upload.uploadFiles.splice(-1, 1);
                    this.$message.error('非法文件格式');
                    return false;
                }
            }
            if (this.maxSize) {
                let isValid = File.size / 1024 / 1024 <= this.maxSize;
                if (!isValid) {
                    // 上传失败 将上传失败的文件信息删除
                    this.$refs.upload.uploadFiles.splice(-1, 1);
                    this.$message.error(`上传文件大小不能超过${this.maxSize}MB`);
                    return false;
                }
            }
            this.disabled = true;
            this.hasFile = false;
            this.hasUploadChunk = [];
            // 不支持多文件时 再次上传文件删除之前的文件
            if (!this.multiple && fileList.length > 1) {
                fileList.shift();
            }
            let self = this;
            // 获取用户选择的文件
            const file = File.raw;
            // 文件大小(大于100m再分片哦,否则直接走普通文件上传的逻辑就可以了,这里只实现分片上传逻辑)
            const fileSize = File.size;
            // 可以设置大于多少兆可以分片上传,否则走普通上传
            if (fileSize <= chunkSize) {
                this.$refs.upload.submit();
                return;
            }
            let isValid = fileSize / 1024 / 1024 <= this.maxSize;
            if (!isValid) {
                this.$message.error(`上传文件大小不能超过${this.maxSize}MB`);
                this.hasFile = false;
                this.disabled = false;
                this.$refs.upload.clearFiles();
                return false;
            }
            // 计算当前选择文件需要的分片数量
            const chunkCount = Math.ceil(fileSize / chunkSize) || 1;
            console.log('文件大小:', (File.size / 1024 / 1024) + 'Mb', '分片数:', chunkCount);
            const fileMd5 = await self.getFileMd5(file, chunkCount);
            // 并发上传
            const result = await this.asyncPool(limitPool, chunkCount, i => {
                const chunks
                    = typeof chunkSize === 'number' ? Math.ceil(fileSize / chunkSize) : 1;
                let start = i * chunkSize;
                let end = i + 1 === chunks ? fileSize : (i + 1) * chunkSize;
                const chunk = file.slice(start, end);
                return this.getMethods(chunk, fileMd5, i);
            });
            if (result) {
                // 文件上传完毕,请求后端合并文件并传入参数
                self.composeFile(fileMd5, File.name, chunkCount);
            }
        },
        // 上传文件方法
        getMethods(chunk, fileMd5, i) {
            let formdata = new FormData();
            formdata.append('file', chunk);
            formdata.append('fileMd5', fileMd5 + '-' + i);
            return request.post(apis.url.fragmentFile + '/upload?chunkNum=' + i, formdata).then(res => {
                return res;
            });
        },
        // 设置并发
        async asyncPool(poolLimit, array, iteratorFn) {
        	// 存储所有的异步任务
            const ret = [];
            // 存储正在执行的异步任务
            const executing = [];
            for (let item = 0; item < array; item++) {
                const p = iteratorFn(item, array);
                // 保存新的异步任务
                ret.push(p);
                // 当poolLimit值小于或等于总任务个数时,进行并发控制
                if (poolLimit <= array.length) {
                	// 当任务完成后,从正在执行的任务数组中移除已完成的任务
                    const e = p.then(() => executing.splice(executing.indexOf(e), 1));
                    // 保存正在执行的异步任务
                    executing.push(e);
                    if (executing.length >= poolLimit) {
                    	// 等待较快的任务执行完成
                        await Promise.race(executing);
                    }
                }
            }
            return Promise.all(ret).then(res => {
                this.hasUploadChunk = res.map(reslut => {
                    return reslut.data.md5;
                });
                return true;
            }, err => {
                this.$message.error('上传错误');
                this.disabled = false;
                this.$refs.upload.clearFiles();
                return false;
            });
        },
        // 获取文件MD5(分段读取文件,减少内存的使用)
        getFileMd5(file, chunkCount) {
            return new Promise((resolve, reject) => {
            	// 用来创建文件的一个片段 (也就是一个 Blob),而不会加载整个文件
                let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
                let chunks = chunkCount;
                let currentChunk = 0;
                // 创建一个新的 SparkMD5 对象,用来计算 MD5 哈希值。
                let spark = new SparkMD5.ArrayBuffer();
                // 创建一个新的 FileReader 对象,用来异步读取文件的内容。
                let fileReader = new FileReader();

                fileReader.onload = function (e) {
                    spark.append(e.target.result);
                    currentChunk++;
                    if (currentChunk < chunks) {
                        loadNext();
                    }
                    else {
                        let md5 = spark.end();
                        resolve(md5);
                    }
                };
                fileReader.onerror = function (e) {
                    reject(e);
                };
                // 根据当前的块编号和块大小,从文件中读取下一个块,然后添加到 SparkMD5 对象中。
                function loadNext() {
                    let start = currentChunk * chunkSize;
                    let end = start + chunkSize;
                    if (end > file.size) {
                        end = file.size;
                    }
                    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
                }
                loadNext();
            });
        },
        /**
         * 请求后端合并文件
         * @param fileMd5 文件md5
         * @param fileName 文件名称
         * @param count 文件分片总数
         */
        composeFile(fileMd5, fileName, count) {
            console.log('开始请求后端合并文件');
            let data = {
                'afterMd5': fileMd5, // 文件的md5
                'filename': fileName, // 文件名
                'files': this.hasUploadChunk // 分片的MD5
            };
            request.post(apis.url.fragmentFile + '/merge', data).then(res => {
                if (res && res.code === 200) {
                    this.$message.success(this.successTip);
                    this.hasFile = true;
                    this.$emit('success', res.data.fileKey, fileName);
                }
                else {
                    this.$refs.upload.clearFiles();
                }
            }).catch(err => {
                this.$refs.upload.clearFiles();
            });
            this.disabled = false;
        },
        onRemove(file, fileList) {
            this.hasFile = false;
            this.disabled = false;
            this.$refs.upload.clearFiles();
            this.$emit('remove', file, fileList);
        },
        onSuccess(res, file, fileList) {
            if (typeof this.successCallback === 'function') {
                this.successCallback(res);
            }
            else {
                this.$message.success(this.successTip);
                this.hasFile = true;
                this.$emit('success', res.data.key, res.data.fileName);
            }
            this.$nextTick(res => {
                this.disabled = false;
                this.hasFile = true;
            });
        },
        onError(err, file, fileList) {
            try {
                err = JSON.parse(err.message);
            }
            catch (e) {
                err = {};
            }
            if (typeof this.errorCallback === 'function') {
                return this.errorCallback(err);
            }

            this.$message.error(err && err.msg || this.errorTip);
            this.$emit('error', err, file, fileList);
        }
    },
};

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值