断点续传
思路
1.通过md5 来分别对每一个文件分片来进行编码
2.对整个文件进行md5的编码
3.通过并发请求上传分片
4.上传完成的时候通知后端进行分片的合并,合并成一个文件,返回文件地址
5.如果中间中断了请求,再次上传文件的时候重新对整个文件以及文件的分片进行编码,调用接口去后端查询已经上传的分片,在对未上传的分片进行 并发请求上传分片,结束的时候通知后端合并文件
注意事项:
1.在上传不同的文件的时候,当文件改变的时候,需要重置下存储文件信息的数组,避免上一个文件的信息影响
//文件改变的时候
const fileChange = async (e) => {
const { files } = e.target;
//重置下数据
resetChunkInfor();
await splitChunk(files[0], size);
searchFile();
};
2.在向后端查询已经上传完成的数据,需要前端将整个文件的数据删除掉 已经上传的数据,需要注意的是:如果是通过index的顺序来进行数据的删除的话,删除的数组需要在最里层来进行遍历删除。否则会引起删除数据不准的问题。
const getFileInfor = (fileDate) => {
getinfor(fileDate)
.then((res) => {
if (res.code == 200) {
chunkInfor.finishFilds = res.finish;
//更新需要上传的数据
res.finish.map((demo) => {
chunkInfor.filds.forEach((item, index) => {
//代表已经上传这个分片了
if (demo == item) {
//删除该分片编码
chunkInfor.filds.splice(index, 1);
//删除分片数据
chunkInfor.fileBuffer.splice(index, 1);
}
});
});
if (res.fileUrl) {
fileUrl.value = res.fileUrl;
showPro.value = false;
} else {
showPro.value = true;
fileUrl.value = "";
}
}
})
.catch((error) => {
console.log(error);
});
};
前端代码:
<template>
<input type="file" @change="fileChange" />
<el-button type="primary" @click="upChunks">上传</el-button>
<el-progress v-if="showPro" :percentage="percentage" />
<div>上传完成的文件地址:{{ fileUrl }}</div>
<!-- <el-button @click="searchFile">查询数据</el-button> -->
<!-- <el-button @click="conative">合并文件</el-button> -->
</template>
<script lan="js" setup>
import SparkMD5 from "spark-md5";
// import buffer from "buffer";
import { demoApi, getinfor, concatInfor } from "@/api/demo.js";
//MIME类型映射
import { mimeName, mimeType } from "../untils/MimeType";
import { computed, reactive, ref } from "vue";
// import { isArray } from "element-plus/es/utils";
const chunkInfor = reactive({
fileMd: "", //存储文件的md5
filds: [], //存储每一个分片的编码
allFilds: [], //需要上传的全部分片编码
fileBuffer: [], // 存储文件的buffer 数据
fileType: "", // 文件的类型
finishFilds: [] // 存储上传完成的分片数据
});
const fileUrl = ref("");
const showPro = ref(true);
const percentage = computed(() => {
return Math.floor((chunkInfor.finishFilds.length / chunkInfor.allFilds.length) * 100 || 0);
});
const fileChange = async (e) => {
const { files } = e.target;
//重置下数据
resetChunkInfor();
await splitChunk(files[0], size);
searchFile();
};
//重置下数据
const resetChunkInfor = () => {
Object.assign(chunkInfor, {
fileMd: "",
filds: [],
allFilds: [],
fileBuffer: [],
fileType: "",
finishFilds: []
});
};
//查询数据
const searchFile = () => {
let { fileMd, filds, fileType } = chunkInfor;
getFileInfor({
fileMd,
filds,
fileType
});
};
// 查询后端存储的文件信息
const getFileInfor = (fileDate) => {
getinfor(fileDate)
.then((res) => {
if (res.code == 200) {
chunkInfor.finishFilds = res.finish;
//更新需要上传的数据
res.finish.map((demo) => {
chunkInfor.filds.forEach((item, index) => {
//代表已经上传这个分片了
if (demo == item) {
//删除该分片编码
chunkInfor.filds.splice(index, 1);
//删除分片数据
chunkInfor.fileBuffer.splice(index, 1);
}
});
});
if (res.fileUrl) {
fileUrl.value = res.fileUrl;
showPro.value = false;
} else {
showPro.value = true;
fileUrl.value = "";
}
}
})
.catch((error) => {
console.log(error);
});
};
//合并文件
const conative = () => {
let { fileMd, filds, fileType } = chunkInfor;
concatfileInfor({
fileMd,
filds,
fileType
});
};
//合并文件
const concatfileInfor = (fileDate) => {
// fileDate
concatInfor(fileDate)
.then((res) => {
if (res.code == 200) {
fileUrl.value = res.url;
}
})
.catch((error) => {
console.log(error);
});
};
//10kB 进行切片
const size = 10 * 1024;
// 生成文件的切片
const splitChunk = (file, size = 10 * 1024) => {
return new Promise(async (resolve, reject) => {
//文件以字节为单位进行分割的
// 用来存储 buffer 的数组
const spark = new SparkMD5.ArrayBuffer();
//分片的数量
const chunkNumber = Math.ceil(file.size / size);
//存储上传文件的类型
if (chunkInfor.fileType == "") {
mimeType.forEach((item, index) => {
if (file.type == item) {
chunkInfor.fileType = mimeName[index];
}
});
}
//当前分片的下标
let chunkIndex = 0;
// chunkDemo();
// 生成文件读取器
const fileReader = new FileReader();
//文件读取成功
fileReader.onload = async function (e) {
// 将文件存储在buffer数组中
spark.append(e.target.result);
//存储文件的buffer数据
chunkInfor.fileBuffer.push(new Blob([e.target.result]));
// chunkInfor.fileBuffer.push(buffer.Buffer.from(e.target.result));
//对当前分片进行md5 的编码
const cunkMd5 = await SparkMD5.ArrayBuffer.hash(e.target.result);
//放入每一个分片md5
chunkInfor.filds.push(cunkMd5 + chunkIndex);
chunkInfor.allFilds.push(cunkMd5 + chunkIndex);
chunkIndex++;
if (chunkIndex < chunkNumber) {
//读取下一片分片
readNext();
} else {
//对整个文件进行md5 加密处理
chunkInfor.fileMd = spark.end();
resolve(chunkInfor);
}
};
//读取下一个分片
async function readNext() {
const start = chunkIndex * size;
const end =start + size >= file.size ? file.size : start + size;
// 使用文件读取器读取该文件
fileReader.readAsArrayBuffer(file.slice(start, end));
}
readNext();
});
};
//上传切片
const upChunks = async () => {
const finishresult = await concurRequest(chunkInfor.filds.length, 3);
};
//并发请求的封装
const concurRequest = async (allRequestNumber, maxnumber = 3) => {
/**
* 1.循环maxnumber 进行请求的发送
* 2.在发送请求中 存储下 index, 用来回填请求成功之后的
*/
let response = []; //用来存储每次请求的返回的结果
let count = 0; // 用来存储接口请求成功或者失败的数量
let index = 0; //下一次请求的index
return new Promise((resolve, reject) => {
function sendRequest() {
const data = {
fileMd: chunkInfor.fileMd,
filds: chunkInfor.filds[index],
fileBuffer: chunkInfor.fileBuffer[index]
};
const formData = new FormData();
formData.append("fileMd", data.fileMd);
formData.append("filds", data.filds);
formData.append("file", data.fileBuffer);
// 存储本次请求的index
let seleIndex = index;
index++;
demoApi(formData)
.then((res) => {
count++;
if (count > allRequestNumber - 1) {
resolve(response);
//合并文件接口
conative();
}
if (index <= allRequestNumber - 1) {
//递归调用
sendRequest();
}
response[seleIndex] = "请求成功" + seleIndex;
//存储上传成功的分片 计算上传进度条
chunkInfor.finishFilds.push(data.filds);
})
.catch((error) => {
count++;
if (count > allRequestNumber - 1) {
resolve(response);
//合并文件接口
conative();
}
if (index <= allRequestNumber - 1) {
//递归调用
sendRequest();
}
response[seleIndex] = "请求失败" + seleIndex;
})
}
for (let i = 0; i < Math.min(allRequestNumber, maxnumber); i++) {
sendRequest();
}
});
};
</script>