分为以下几步:
- 1.前端接收BGM并进行
切片 - 2.将每份
切片都进行上传 - 3.后端接收到所有
切片,创建一个文件夹存储这些切片 - 4.后端将此
文件夹里的所有切片合并为完整的文件 - 5.删除
文件夹,因为切片不是我们最终想要的,可删除 - 6.当服务器已存在某一个文件时,再上传需要实现
“秒传”
文件切片
- 为什么要切片?
一个文件过大,上传会非常的慢,而且还可能会中途失败导致前功尽弃。要重新上传文件,非常影响用户体验。文件切片以后,可以并发上传,速度比之前更快。而且如果中途失败可以做到不用上传已经上传过的。
- 对文件如何切片?
通过Blob.prototype.slice方法即可对文件进行切分,分片尽量不要太大,一般最大50M即可。
- 相关实现如下
const maxChunkSize = 52428800 // 最大容量块
const chunkSum = Math.ceil(file?.size / maxChunkSize)
export const createFileChunks = async ({file, chunkSum, setProgress}) => {
const fileChunkList = [];
const chunkSize = Math.ceil(file?.size / chunkSum);
let start = 0;
for (let i = 0; i < chunkSum; i++) {
const end = start + chunkSize;
fileChunkList.push({
index: i,
filename: file?.name,
file: file.slice(start, end)
});
start = end;
}
const result = await getFileHash({chunks: fileChunkList, setProgress});
fileChunkList.map((item, index) => {
item.key = result;
});
return fileChunkList;
};
具体实现方案
解决方案一: html部分采用el-upload组件,具体代码如下:
<el-upload
class="upload-demo"
action="https://jsonplaceholder.typicode.com/posts/"
:on-change="handleChange"
:http-request="putinMirror"
:file-list="fileList">
<el-button size="small" type="primary">点击上传</el-button>
<!-- <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> -->
</el-upload>
js部分代码如下:
export default {
name: "JsPage",
data() {
return {
fileList: [{
name: 'food.jpeg',
url: ''
}, {
name: 'food2.jpeg',
url: ''
}]
}
},
mounted() {},
methods: {
handleChange(file, fileList) {
console.log('文件上传change事件:',file, fileList)
//this.fileList = fileList.slice(-3);
},
// 覆盖组件默认的上传行为,可以自定义上传的实现
async putinMirror(file) {
// 每个文件切片大小定为5MB
let sliceSize = 0.5 * 1024 * 1024;
// 文件大小限制为最大1个G,可根据需求修改
let maxfilesizes = 1 * 1024 * 1024 * 1024;
console.log('putinMirror:',file)
const blob = file.file;
const fileSize = blob.size;// 文件大小
const fileName = blob.name;// 文件名
//计算文件切片总数,Math.ceil向上取整数
const totalSlice = Math.ceil(fileSize / sliceSize);
console.log('当前上传文件的详情信息',blob,totalSlice,fileSize / sliceSize)
if(fileSize <= maxfilesizes){
// 循环上传
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.append("file", chunk);
formData.append("signal", blob.uid);
formData.append("name", fileName);
formData.append("size", fileSize);
formData.append("chunks", totalSlice);
formData.append("chunk", i+1);
let res = await this.uploadExcleAndZip(formData);//uploadExcleAndZip模拟接口上传,一个分片上传完成后再调用接口上传下一片
console.log(res);
if(res.errCode == 0){
//this.progress = ((i+1)/totalSlice).toFixed(1) * 100;//控制进度条
setTimeout(()=>{
if((i+1) == totalSlice){
this.$message({
message: '上传成功',
type: 'success'
});
}
}, 1000);
}
}
} else { // 文件大小超出最大限制
this.$message({
message: '文件大小超出1G',
type: 'error'
});
}
},
uploadExcleAndZip(formData) {
console.log('在这里模拟调接口:',formData)
return {
errCode:0
}
}
}
}
秒传和断点续传
前端hash生成
断点续传、秒传功能都需要后端有办法能够判断我们的文件到底上没上传成功,用什么来判断呢?
当然使用文件的 hash 值,同一个文件的 hash 值计算出来是一样的。
我们可以使用使用 spark-md5 库来计算文件的 hash 值。
npm i spark-md5 -S
npm i @types/spark-md5 -D
复制代码
代码片段:
const handleFileUpload = async (file: File) => {
// ...
const spark = new SparkMD5.ArrayBuffer();
for (let cur = 0; cur < file.size; cur += SIZE) {
// ...
spark.append(await file.slice(cur, cur + SIZE).arrayBuffer())
}
const hash = spark.end();
// ...
};
复制代码
文件秒传
要实现断点续传,我就得实现文件秒传,文件秒传的本质就是通过 hash 值的查询判断后端是否已经存在了该文件,如果存在那就不传了,这就是秒传的本质。
前端需要在上传文件之前先调接口查询文件是否存在:
- 如果存在则返回一个
true - 如果不存在则返回
false - 如果上传过一部分,则返回
number[],里面按照chunkIndex存着各个分片的size,以方便进度计算和判断是否需要重新上传该分片
代码片段:
const filename = useRef('');
const fileChunks = useRef<FileChunk[]>([]);
复制代码
const verifyUpload = (filename: string, hash: string) => {
return new Promise<number[]>((resolve, reject) => {
request<number[]>({
url: '/verify',
method: 'POST',
data: { filename, hash },
headers: { 'Content-Type': 'application/json' },
})
.then((res) => {
resolve(res.data);
})
.catch(err => {
reject(err);
});
});
}
const handleFileUpload = async (hash: string) => {
const verifyRes = await verifyUpload(filename.current, hash)
.catch(e => {
console.error(e);
});
if (verifyRes !== undefined) {
if (typeof verifyRes === 'boolean') {
if (verifyRes) {
console.log('文件已经上传过,可以秒传');
setProgress('100');
} else {
POOL.length = 0;
sliceChunks(hash, fileChunks.current.map(() => 0));
}
} else {
POOL.length = 0;
sliceChunks(hash, verifyRes);
}
} else {
console.log('验证失败');
}
};
const handleFileChange = async (evt: ChangeEvent<HTMLInputElement>) => {
const file = (evt.target.files as FileList)['0'];
let chunkIndex = 0;
totalSize.current = file.size;
filename.current = file.name;
const spark = new SparkMD5.ArrayBuffer();
for (let cur = 0; cur < file.size; cur += SIZE) {
fileChunks.current.push({
chunkIndex: chunkIndex++,
chunk: file.slice(cur, cur + SIZE),
});
spark.append(await file.slice(cur, cur + SIZE).arrayBuffer())
}
const hash = spark.end();
progressArr.current = [];
handleFileUpload(hash);
};
复制代码
前面的代码存储上传的文件的时候是用的文件原始的名称,中间使用的临时文件夹也是用的文件的原始名称,从这里开始就改造为使用 hash 值来作为名称了。
断点续传
断点续传就是在前面停止的传了一部分的基础上进行上传,之前传输的信息会通过 verifyUpload 接口进行告知,传完的切片就不会重新传了,没有传或者没有传完的切片就会重新传。
const handleFinishedUploadProgress = (size: number, chunkIndex: number) => {
progressArr.current[chunkIndex] = size * 100;
const curTotal = progressArr.current.reduce(
(accumulator, currentValue) => accumulator + currentValue,
0,
);
setProgress((curTotal / totalSize.current).toFixed(2));
};
const sliceChunks = async (hash: string, chunksSize: number[]) => {
for (let i = 0; i < fileChunks.current.length; i++) {
const fileChunk = fileChunks.current[i];
const formData = new FormData();
formData.append('filename', filename.current);
formData.append('chunkIndex', String(fileChunk.chunkIndex));
formData.append('hash', hash);
formData.append('file', fileChunk.chunk);
if (chunksSize[i] !== fileChunk.chunk.size) { // size一样的说明已经上传完毕了,只传size不一样的
const uplaodTask = uploadFile(formData, i);
uplaodTask.then(() => handleTask(uplaodTask));
POOL.push(uplaodTask);
if (POOL.length === MAX_POOL) {
// 并发池跑完一个任务之后才会继续执行for循环,塞入一个新任务
await Promise.race(POOL);
}
} else {
handleFinishedUploadProgress(chunksSize[i], i);
}
}
Promise.all(POOL)
.then(() => {
mergeFile(filename.current, hash);
});
};