在掘金白嫖许久,想来应该回馈下,为了能够继续白嫖😁前端
很早以前就在掘金看到过关于实现断点续传的文章,但不曾实践过,正好最近项目中也遇到了此场景,便去重温了一遍,从头到底作了实现。ios
技术栈:VUE+Elementui+localstorage+Workerweb
寄语:本人并不是"巨人",只是有幸站在了巨人的肩膀axios
请先阅读如下博文:数组
先写写总结,我的认为,本项目主要难点在于处理多个文件上传时,如何将每一个文件的状态及进度对应到相关的界面展现中。绕了不少坑。缓存
作到重试时,卡了半天,实在想不明白,最后去厕所呆了会,而后就很快写出来了。bash
详细思路各位可参考上面2篇文章,再也不赘述!服务器
//简单粗暴
<input type="file" multiple @change="handleFileChange" />
<el-button @click="handleUpload">上传</el-button>
<el-button @click="handleResume">恢复</el-button>
<el-button @click="handlePause">暂停</el-button>
//js
const SIZE = 50 * 1024 * 1024; // 切片大小, 1M
var fileIndex = 0; // 当前正在被遍历的文件下标
export default {
name: 'SimpleUploaderContainer',
data: () => ({
container: {
hashArr: [], // 存储已计算完成的hash
data: []
},
tempFilesArr: [], // 存储files信息
uploadMax: 3, // 上传时最大切片的个数,
cancels: [] // 存储要取消的请求
})
}
复制代码
handleFileChange(e) {
const files = e.target.files;
console.log('handleFileChange -> file', files);
if (!files) return;
Object.assign(this.$data, this.$options.data()); // 重置data全部数据
fileIndex = 0; // 重置文件下标
this.container.files = files;
// 拷贝filelist 对象
for (const key in this.container.files) {
if (this.container.files.hasOwnProperty(key)) {
const file = this.container.files[key];
var obj = { statusStr: '正在上传', chunkList: [], uploadProgress: 0, hashProgress: 0 };
for (const k in file) {
obj[k] = file[k];
}
this.tempFilesArr.push(obj);
}
}
}
复制代码
建立切片-》-》计算HASH-》判断是否为秒传-》上传切片-》存储已上传的切片下标。 hash计算方式也是经过worker处理。并发
async handleUpload() {
if (!this.container.files) return;
const filesArr = this.container.files;
var tempFilesArr = this.tempFilesArr;
console.log('handleUpload -> filesArr', filesArr);
for (let i = 0; i < filesArr.length; i++) {
fileIndex = i;
const fileChunkList = this.createFileChunk(filesArr[i]);
// hash校验,是否为秒传
const hash = await this.calculateHash(fileChunkList, filesArr[i].name);
console.log('handleUpload -> hash', hash);
const verifyRes = await this.verifyUpload(filesArr[i].name, hash);
if (!verifyRes.data.presence) {
this.$message('秒传');
tempFilesArr[i].statusStr = '已秒传';
tempFilesArr[i].uploadProgress = 100;
} else {
console.log('开始上传文件----》', filesArr[i].name);
const getChunkStorage = this.getChunkStorage(hash);
tempFilesArr[i].fileHash = hash; // 文件的hash,合并时使用
tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
fileHash: hash,
fileName: filesArr[i].name,
index,
hash: hash + '-' + index,
chunk: file,
size: file.size,
uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识:是否已完成上传
progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
status: getChunkStorage && getChunkStorage.includes(index) ? 'success' : 'wait' // 上传状态,用做进度状态显示
}));
console.log('handleUpload -> this.chunkData', tempFilesArr[i]);
await this.uploadChunks(this.tempFilesArr[i]);
}
}
}
// 建立文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
var count = 0;
while (count < file.size) {
fileChunkList.push({
file: file.slice(count, count + size)
});
count += size;
}
return fileChunkList;
}
// 存储已上传完成的切片下标
addChunkStorage(name, index) {
const data = [index];
const arr = getObjArr(name);
if (arr) {
saveObjArr(name, [...arr, ...data]);
} else {
saveObjArr(name, data);
}
},
// 获取已上传完成的切片下标
getChunkStorage(name) {
return getObjArr(name);
}
// 生成文件 hash(web-worker)
calculateHash(fileChunkList, name) {
return new Promise((resolve) => {
this.container.worker = new Worker('./hash/md5.js');
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = (e) => {
const { percentage, hash } = e.data;
//当时想将每一个文件的hash放在同一个节目展现,因此就存在了一个数组里
this.tempFilesArr[fileIndex].hashProgress = percentage;
if (hash) {
resolve(hash);
}
};
});
}
复制代码
// 将切片传输给服务端
async uploadChunks(data) {
var chunkData = data.chunkList;
const requestDataList = chunkData
.filter(({ uploaded }) => !uploaded)
.map(({ fileHash, chunk, fileName, index }) => {
const formData = new FormData();
formData.append('md5', fileHash);
formData.append('file', chunk);
formData.append('fileName', index);
return { formData, index, fileName };
});
try {
const ret = await this.sendRequest(requestDataList, chunkData);
console.log('uploadChunks -> chunkData', chunkData);
console.log('ret', ret);
data.statusStr = '上传成功';
} catch (error) {
// 上传有被reject的
data.statusStr = '上传失败,请重试';
this.$message.error('亲 上传失败了,考虑重试下呦');
return;
}
// 合并切片
const isUpload = chunkData.some((item) => item.uploaded === false);
console.log('created -> isUpload', isUpload);
if (isUpload) {
alert('存在失败的切片');
} else {
// 执行合并
await this.mergeRequest(data);
}
复制代码
重试机制参考的也是上面博文:
并发:经过for循环控制起始值,在函数体内进行递归调用,便达到了并发的效果。
// 并发处理
sendRequest(forms, chunkData) {
console.log('sendRequest -> forms', forms);
console.log('sendRequest -> chunkData', chunkData);
var finished = 0;
const total = forms.length;
const that = this;
const retryArr = []; // 数组存储每一个文件hash请求的重试次数,作累加 好比[1,0,2],就是第0个文件切片报错1次,第2个报错2次
return new Promise((resolve, reject) => {
const handler = () => {
console.log('handler -> forms', forms);
if (forms.length) {
// 出栈
const formInfo = forms.shift();
const formData = formInfo.formData;
const index = formInfo.index;
instance
.post('fileChunk', formData, {
onUploadProgress: that.createProgresshandler(chunkData[index]),
cancelToken: new CancelToken((c) => this.cancels.push(c)),
timeout: 0
})
.then((res) => {
console.log('handler -> res', res);
// 更改状态
chunkData[index].uploaded = true;
chunkData[index].status = 'success';
// 存储已上传的切片下标
this.addChunkStorage(chunkData[index].fileHash, index);
finished++;
handler();
})
.catch((e) => {
console.warn('出现错误', e);
console.log('handler -> retryArr', retryArr);
if (typeof retryArr[index] !== 'number') {
retryArr[index] = 0;
}
// 更新状态
chunkData[index].status = 'warning';
// 累加错误次数
retryArr[index]++;
// 重试3次
if (retryArr[index] >= 3) {
console.warn(' 重试失败--- > handler -> retryArr', retryArr, chunkData[index].hash);
return reject('重试失败', retryArr);
}
console.log('handler -> retryArr[finished]', `${chunkData[index].hash}--进行第 ${retryArr[index]} '次重试'`);
console.log(retryArr);
this.uploadMax++; // 释放当前占用的通道
// 将失败的从新加入队列
forms.push(formInfo);
handler();
});
}
console.log('handler -> total', total);
console.log('handler -> finished', finished);
if (finished >= total) {
resolve('done');
}
};
// 控制并发
for (let i = 0; i < this.uploadMax; i++) {
handler();
}
});
}
复制代码
// 切片上传进度
createProgresshandler(item) {
return (p) => {
item.progress = parseInt(String((p.loaded / p.total) * 100));
this.fileProgress();
};
}
// 文件总进度
fileProgress() {
//经过全局变量 fileIndex 定位当前正在传输的文件。
const currentFile = this.tempFilesArr[fileIndex];
const uploadProgress = currentFile.chunkList.map((item) => item.size * item.progress).reduce((acc, cur) => acc + cur);
const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2));
currentFile.uploadProgress = currentFileProgress;
}
复制代码
mergeRequest(data) {
const obj = {
md5: data.fileHash,
fileName: data.name,
fileChunkNum: data.chunkList.length
};
instance.post('fileChunk/merge', obj,
{
timeout: 0
})
.then((res) => {
// 清除storage
clearLocalStorage(data.fileHash);
this.$message.success('上传成功');
});
}
复制代码
本项目使用的是axios,暂停的关键就是取消当前的请求,axios也提供了方法,咱们须要简单处理下。
handlePause() {
while (this.cancels.length > 0) {
this.cancels.pop()('取消请求');
}
}
//在并发请求除,存储了当前正在传输的请求,调用每一个请求的cancels方法便可

复制代码