上传文件遇到大文件的时候,非常影响上传速度,有时候会文件无响应,这时候需要用到文件上传,文件上传是一个常见的功能需求,原理通过将大文件切分成多个小块(切片)进行上传,不仅可以有效减少单次上传的数据量,降低网络波动对上传过程的影响,还能实现如断点续传、秒传等功能。
1.选择文件
2.计算文件的MD5值
3.校验文件是否需要上传(通过后端接口查询)
4.文件切片
5.并发上传切片 (浏览器最大请求数6个)
6.通知后端合并切片
第一步:选择文件
<template>
<div>
<input type="file" @change="handleFileChange" accept="video/*" />
</div>
</template>
async handleFileChange(event){
if (!event.target.files[0]) return; // 未选择文件则返回
},
第二步:计算文件 MD5
使用spark-md5 进行文件加密,使得同一个文件的MD5是一样的
//安装md5 npm i spark-md5
async computeFileHash(file) {
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
return new Promise((resolve) => {
fileReader.onload = (e) => {
spark.append(e.target.result);
const hash = spark.end();
resolve(hash);
};
fileReader.readAsArrayBuffer(file);
});
}
第三步:校验文件是否需要上传(通过后端接口查询)
通过调后端查询接口,判断是否需要上传,不需要直接跳过(秒传)
checkFile(fileHash){
//调用后端接口判断文件是否上传过
//模拟接口 shouldUpload:是否应该上传,uploadedChunks:已经上传的切片
return { shouldUpload: true, uploadedChunks: [] };
},
第四步:文件切片
根据接口返回值判断是否需要上传,需要上传的进行切片,并加入请求队列准备上传
sliceFileAndUpload(fileHash, uploadedChunks){
const chunkSize = 10 * 1024 * 1024; // 切片大小,这里是10MB
this.chunkCount = Math.ceil(this.selectedFile.size / chunkSize); // 计算总切片数
for (let i = 0; i < this.chunkCount; i++) {
if (uploadedChunks.includes(i)) continue; // 跳过已上传的切片
const chunk = this.selectedFile.slice(i * chunkSize, (i + 1) * chunkSize); // 获取切片
this.requestPool.push({ chunk, index: i }); // 加入请求池
}
this.processPool(fileHash); // 开始处理请求池
},
第五步:并发上传切片
采用并发上传,控制上传的个数,上传完成一个切片后将取出一个切片放入请求队列中,实现最大化。
processPool(fileHash){
while (this.requestPool.length > 0 && this.MAX_REQUEST > 0) {
const { chunk, index } = this.requestPool.shift(); // 取出一个待上传的切片
this.uploadChunk(chunk, fileHash, index) // 上传切片
.then(() => {
this.uploadedChunksCount++; // 更新已上传切片数量
if (this.requestPool.length > 0) {
this.processPool(fileHash); // 继续处理请求池
} else if (this.uploadedChunksCount === this.chunkCount) {
//TODO 所有切片都已上传,通知服务器合并
}
})
.finally(() => {
this.MAX_REQUEST++; // 释放一个请求槽
});
this.MAX_REQUEST--; // 占用一个请求槽
}
},
async uploadChunk(chunk, fileHash, index){
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", fileHash);
formData.append("index", index);
// 替换为真实的上传URL
// await axios.post("xxx", formData);
}
第六步:通知后端合并切片
所以切片上传完成后,通知后端去合并切片,即一个切片上传完成。
mergeChunkFiles(fileHash){
// axios.post("xxx", fileHash);
}
完整代码
<template>
<div>
<input type="file" @change="handleFileChange" accept="video/*" />
</div>
</template>
<script>
import SparkMD5 from "spark-md5"; // 引入SparkMD5用于计算文件的MD5值
export default {
name: 'upload',
data(){
return{
selectedFile:null,//选中的文件
chunkCount:0,//切片总数
requestPool:[],//请求队列
MAX_REQUEST:6,//最大并发量
uploadedChunksCount: 0, // 已上传的切片数量
}
},
methods:{
async handleFileChange(event){
if (!event.target.files[0]) return; // 未选择文件则返回
this.selectedFile=event.target.files[0]
const fileHash = await this.computeFileHash(event.target.files[0]); // 计算文件hash
const { shouldUpload, uploadedChunks } = await this.checkFile(fileHash); // 检查文件是否需要上传
if(!shouldUpload){
console.log('秒传成功');
return;
}
this.sliceFileAndUpload(fileHash, uploadedChunks); // 切片并上传
},
computeFileHash(file){
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result);
const hash = spark.end();
resolve(hash); // 返回计算得到的hash值
};
fileReader.readAsArrayBuffer(file);
});
},
checkFile(fileHash){
//调用后端接口判断文件是否上传过
//模拟接口 shouldUpload:是否应该上传,uploadedChunks:已经上传的切片
return { shouldUpload: true, uploadedChunks: [] };
},
sliceFileAndUpload(fileHash, uploadedChunks){
const chunkSize = 10 * 1024 * 1024; // 切片大小,这里是10MB
this.chunkCount = Math.ceil(this.selectedFile.size / chunkSize); // 计算总切片数
for (let i = 0; i < this.chunkCount; i++) {
if (uploadedChunks.includes(i)) continue; // 跳过已上传的切片
const chunk = this.selectedFile.slice(i * chunkSize, (i + 1) * chunkSize); // 获取切片
this.requestPool.push({ chunk, index: i }); // 加入请求队列
}
this.processPool(fileHash); // 开始处理请求队列
},
processPool(fileHash){
while (this.requestPool.length > 0 && this.MAX_REQUEST > 0) {
const { chunk, index } = this.requestPool.shift(); // 取出一个待上传的切片
this.uploadChunk(chunk, fileHash, index) // 上传切片
.then(() => {
this.uploadedChunksCount++; // 更新已上传切片数量
if (this.requestPool.length > 0) {
this.processPool(fileHash); // 继续处理请求队列
} else if (this.uploadedChunksCount === this.chunkCount) {
//TODO 所有切片都已上传,通知服务器合并
this.mergeChunkFiles(fileHash);
}
})
.finally(() => {
this.MAX_REQUEST++; // 释放一个请求槽
});
this.MAX_REQUEST--; // 占用一个请求槽
}
},
async uploadChunk(chunk, fileHash, index){
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", fileHash);
formData.append("index", index);
// 替换为真实的上传URL
// await axios.post("xxx", formData);
},
mergeChunkFiles(fileHash){
// axios.post("xxx", fileHash);
}
}
}
</script>