一般上传文件时会因为ngnix对上传文件大小进行了限制、文件过大接口请求超时导致上传失败,此时可以考虑分片上传,分片就是说将文件拆分来进行上传,将各个文件的切片传递给后台,然后后台再进行合并(合并操作可由前端触发,也可由后端触发,本文由后端触发,前端触发可参考分片上传前端触发合并示例),切片就是文件切分字节(类似于字符串的截取)。
上传文件时需要一个文件的唯一标识,在上传文件过程中需要将文件的唯一标识(文件的唯一标识__该文件的MD5加密文件的字符串)传递给后端,后端会通过该标识返回给我们该上传文件目前的上传状态:
1、文件已存在,文件已经上传,此时我们无需再进行任何操作。
2、部分上传或者尚未上传,把需要上传的分片进行上传
实现目标:
- 简单封装分片上传组件
- 可在其他vue的页面应用使用
- 自动触发上传
实现方式
- 新建vue文件,单独封装该上传组件
- 在vue页面触发使用
1、安装spark-md5
文件的MD5唯一标识,可通过spark-md5来获取,具体步骤:
npm i spark-md5 --save
2、单独封装切片所需js文件(本文起名为chunkFile.js),获取文件的唯一MD5标识码、切片大小等
import SparkMD5 from 'spark-md5'
/**
* 获取文件MD5唯一标识码
* @param file
* @returns {Promise<unknown>}
*/
export function getFileMd5(file, chunkCount) {
return new Promise((resolve, reject) => {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
const chunks = chunkCount
let currentChunk = 0
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
fileReader.onload = function(e) {
spark.append(e.target.result)
currentChunk++
if (currentChunk < chunks) {
loadNext()
} else {
const md5 = spark.end()
resolve(md5)
}
}
fileReader.onerror = function(e) {
reject(e)
}
function loadNext() {
const start = currentChunk * chunkSize
let end = start + chunkSize
if (end > file.size) {
end = file.size
}
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
}
loadNext()
})
}
// 切片大小,自定义
export const chunkSize = 5 * 1024 * 1024
3、封装分片上传组件(本文起名ChunksUpload.vue)
<template>
<div class="uoploadBtn">
<el-button type="primary" :size="size" plain @click.native="clickBtn">{{ title }}</el-button>
<input v-if="fileUpload" ref="fileUpload" type="file" style="width: 0px;height: 0px" @change="fileChange">
</div>
</template>
<script>
import { chunkSize, getFileMd5 } from './chunkFile.js'
import request from './request';
export default {
name: 'ChunksUpload',
props: {
actionUrl: { //文件上传服务地址
type: String,
default: ''
},
chunksUrl: { //检测文件上传服务地址
type: String,
default: ''
},
size: {
type: String,
default: 'mini'
},
title: {
type: String,
default: '上传文件'
},
params: {
type: Object,
default: function() {
return {};
}
}
},
data() {
return {
fileUpload: false
};
},
watch: {},
methods: {
clickBtn() {
const that = this;
this.fileUpload = true;
this.$nextTick(() => {
that.$refs.fileUpload.click();
});
},
/**
* @description:新增
* @param {*} loading
* @return {*}
*/
insertReq(loading, params, formDataFile) {
return request({
url: this.actionUrl,
method: 'post',
params: params,
data: formDataFile,
timeout: 600000
});
},
/**
* @description: 上传文件发生变化时触发
* @param {*}
* @return {*}
*/
fileChange() {
const file = this.$refs.fileUpload.files[0];
if (!(file?.name.endsWith('.zip'))) {
this.$message.error('请选择扩展名为.zip的文件包');
return;
}
this.CheckChunckInfo(file, this.insertReq);
},
async CheckChunckInfo(file, ReqMethod) {
const checkloading = this.$loading({
lock: true,
text: '上传文件检测中...',
background: 'rgba(0, 0, 0, 0.7)'
});
const fileSize = file.size;
const chunkCount = Math.ceil(fileSize / chunkSize);
console.log('文件大小:', (File.size / 1024 / 1024) + 'Mb', '分片数:', chunkCount);
const fileMd5 = await getFileMd5(file, chunkCount);
const params = {
'identifier': fileMd5, // 文件的md5
'filename': file.name// 文件名
};
try {
const response = await request({ url: this.chunksUrl, method: 'get', params });
checkloading.close();
this.fileUpload = false;
if (response.code !== 0) {
this.$message.warning(response?.msg);
return;
}
const res = response.data;
if (!res.exist) {
// 没有完全上传就继续走分片上传
const loading = this.$loading({
lock: true,
text: '正在上传,请等待...',
background: 'rgba(0, 0, 0, 0.7)'
});
this.beforeUpload(ReqMethod, loading, res, chunkCount, fileMd5, file);
} else {
this.$message.warning(file.name + ' 该压缩文件已上传,请重新选择!');
return;
}
} catch (e) {
this.fileUpload = false;
checkloading.close();
}
},
async beforeUpload(ReqMethod, loading, res, chunkCount, fileMd5, file) {
const self = this;
// 获取后端返回的已上传分片数字的数组
const uploaded = res.chunks;
const totalChunk = Array.from(Array(chunkCount).keys(), n => n + 1);
const notUpload = totalChunk.filter(v => !uploaded.includes(v));
const cycleCompute = [];
// 此处将未上传的切片按照每5个为一组进行分组,为后面每次批量上传的5个切片做准备,该数量可根据自己情况修改
if (notUpload.length) {
notUpload.reduce(function(pre, item, index, notUpload) {
var begin = index * 5;
var end = begin + 5;
var res = notUpload.slice(begin, end);
if (res.length != 0) {
cycleCompute[index] = res;
}
}, []);
} else {
loading.close();
this.$message.error(file.name + '该压缩文件已上传,请重新选择!');
return;
}
// 循环调用上传
if (cycleCompute.length) {
for (let c = 0; c < cycleCompute.length; c++) {
const reqArr = [];
const cycleItem = cycleCompute[c];
for (let n = 0; n < cycleItem.length; n++) {
// 定义分片开始上传的序号
const i = cycleItem[n] - 1;
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const _chunkFile = file.slice(start, end);
const formDataFile = new FormData();
const params = {
identifier: fileMd5,
total_chunks: chunkCount,
chunk_numer: cycleItem[n],
filename: file.name,
...self.params //自定义上传的参数
};
formDataFile.append('file', _chunkFile);
reqArr.push(ReqMethod(loading, params, formDataFile));
}
try {
const responselist = await Promise.all(reqArr);
const errprRes = responselist.filter(item => { return !item || item.code != 0; });
if (errprRes.length) {
loading.close();
errprRes.forEach(resItem => {
this.$message.error(resItem.msg);
});
this.$emit('afterSave', false);
return;
}
} catch (e) {
loading.close();
return;
}
}
}
loading.close();
this.$message.success('上传成功');
this.$emit('afterSave', true);
}
}
};
</script>
<style scoped>
.uoploadBtn{
display: inline-block;
margin: 0px 12px
}
</style>