概要
项目中经常会遇到文件 size 特别大的 动不动就 1g、2g 这样在上传过程中就会
文件过大,超出服务端的请求大小限制;
请求时间过长,请求超时;
传输中断,必须重新上传导致前功尽弃;
补充:
前端上传文件总体来说常用的两种方式:二进制传输和base64格式直接传输
正文开始之前先简单认识一下文件上传的四个相关对象:
1.files对象:
可以通过指定
input
标签type
属性为file
来读取files对象,是一个由一个或多个文件对象组成的数组。同时也是blob
对象的子类,继承了一些blob
对象的方法2.blob对象:
表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据, 使用构造函数创建。
3.formData对象:
FormData就是 XMLHttpRequest Level 2 新增的一个对象,利用它来提交表单、模拟表单提交,最大的优势就是可以上传二进制文件。
作用1:模拟HTML表单,相当于将HTML表单映射成表单对象,自动将表单对象中的数据拼接成请求参数的格式。
作用2:异步上传二进制文件。
4.fileReader对象
构造函数方式实例化一个fileReader对象,readAs()方法将文件对象读取成base64格式或者文本格式
解决思路
将获取到的文件 二进制流 按照3兆每片(自定义) 切割成若干片 将这些片依次传给后端 后端在将这些切片拼接起来组合成一个完整的文件 一般大文件上传 会进行md5 计算校验
什么是md5 ?
在选择文件获取到文件之后,首先是对文件进行MD5值的计算,然后拿着这个MD5值对查询后端接口此文件是否存在。查询结果分为三种情况,一是不存在,二是已存在,三是部分存在。不存在时对文件分块,然后一块一块上传;已存在时直接使用已存储的文件,即秒传;部分存在时后端会返回不存在的文件块的位置,然后上传对应不存在的块。
代码展示
<el-upload
action=""
:http-request="httprequest"
list-type="picture-card"
:on-change = 'onchange'
:file-list="getinfodata.logFiles">
<i slot="default" class="el-icon-upload"></i>
<div slot="file" slot-scope="{file}">
<div style="padding: 10px;box-sizing: border-box;word-wrap: break-word;font-size: 12px;">
<div>
<div>
文件名称:
<div>
{{file.name}}
</div>
</div>
<div style="font-size: 12px;">
文件大小: {{ getfilesize(file) }}
</div>
<el-progress
v-show="files && file.uid == files.uid && chunkindex<100"
:format="formats"
:color="colors"
:percentage="chunkindex"
:text-inside="true"
text-color="green"
:stroke-width="18"
style="margin-top: 50px;">
</el-progress>
</div>
</div>
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-delete"
@click="handleDownload(file)">
<i class="el-icon-download"></i>
</span>
<span
class="el-upload-list__item-delete"
@click="handleRemove(file)">
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</el-upload>
// 覆盖组件默认的上传行为,可以自定义上传的实现
httprequest(file){
// 每个文件切片大小定义为 5 MB
let sliceSize = 3 * 1024 * 1024;
this.calculateMD5(file.file,sliceSize).then(async (md5) => {
console.log('File MD5:', md5);
let blob = file.file;
const {size:fileSize,name:fileName} = blob;// 文件大小 // 文件名称
//计算文件切片总数,Math.ceil向上取整数
const totalSlice = Math.ceil(fileSize / sliceSize);
this.chunktotle = totalSlice;
console.log('当前上传文件的详情信息',blob,totalSlice, fileSize / sliceSize)
// 循环上传
// 作用域:使用var声明的变量具有函数作用域,而使用let声明的变量具有块级作用域。函数作用域意味着变量在整个函数体内都是可见的,而块级作用域只在当前代码块内有效。
// 声明提升:使用var声明的变量会在其所在作用域的顶部进行声明提升,也就是说在变量声明之前就可以使用它,但其值为undefined。而使用let声明的变量不会进行声明提升,它只能在声明之后才能被访问到。
for(let i = 0; i< totalSlice; i++){
let start = i * sliceSize;
let chunk = blob.slice(start,Math.min(fileSize, start + sliceSize))
console.log('每个切片的信息:',chunk)
const formData = new FormData();
// 根据后端需要的参数去自定义formData的参数名和值
formData.append('chunk',chunk); // 分片后的文件流
formData.append('index',i+1); // 分片索引
formData.append('total',totalSlice); // 共多少个分片
// 在前端进行大文件上传时,通常可以使用 MD5 值计算来验证文件的完整性。
// MD5(Message Digest Algorithm 5)是一种常用的哈希算法,
// 它将任意长度的数据转换为固定长度的唯一哈希值
// 在大文件上传的场景中,前端会将文件分割为多个小块,然后逐个上传这些小块,最后合并成完整的文件。
// 为了确保每个小块在传输过程中没有被篡改或丢失,需要进行校验。
// MD5 值计算的过程如下:
// 将待上传的文件进行分块。
// 对每个分块的数据进行 MD5 值计算。
// 将计算得到的 MD5 值上传到服务器。
// 服务器接收到每个分块后,也对接收到的分块数据进行 MD5 值计算。
// 服务器将计算得到的 MD5 值与前端上传的 MD5 值进行比较,如果一致则说明分块数据传输正常,否则需要重新传输该分块数据。
formData.append('fileMd5',md5); // 整体文件的MD5值
formData.append('partSize',chunk.size); // 整体文件的MD5值
formData.append('uploadId',res.data.upload_id); // init_upload返回的id
formData.append('complete',(i+1) == totalSlice?'true':this.complete == 'cancel'?'cancel':''); // (true最后一分片)/(cancel取消分片上传)
// 调取后端接口 进行分片上传
let resresult = await this.postRequest('/case/chunk/upload',formData);
console.log(i+1,'--------------',resresult);
// 下面代码可以根据自己的业务情况写
if(resresult.data.cancel == 0){
this.chunkindex = 0;
this.complete = '';
break; // 当点击删除切片请求会返回 cancel 字段此时不需要在请求接口提交后续切片数据退出循环即可
}
if(resresult.code == 0){
console.log(Math.ceil((i+1)/totalSlice*100));
console.log((i+1)/totalSlice);
this.chunkindex = Math.ceil((i+1)/totalSlice*100); // 记录进度条当前到了第几个切片
if(resresult.data.url){
this.fileUrl.push({name:fileName,url:resresult.data.url,size:fileSize,uid:file.file.uid});
}
}else{
this.$message.error({duration:0,message:resresult.message,showClose:true});
break;
}
}
})
.catch((error) => {
this.$message.error(error);
});
}
},
// md5 的计算
calculateMD5(file,chunkSize) {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e)=> {
spark.append(e.target.result); // 更新 MD5 值
currentChunk++;
if (currentChunk < chunks) {
this.loadNextChunk(fileReader,currentChunk,chunkSize,file);
} else {
const md5 = spark.end(); // 计算最终 MD5 值
resolve(md5);
}
};
fileReader.onerror = function (e) {
reject(e);
};
this.loadNextChunk(fileReader,currentChunk,chunkSize,file);
});
},
// md5 计算
loadNextChunk(fileReader,currentChunk,chunkSize,file) {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
fileReader.readAsArrayBuffer(chunk);
},