大文件切片上传
<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);
}
},
};