0%

Vue实现大文件分片上传,包括断点续传以及上传进度条

首先解释一下什么是分片上传

​ 分片上传就是把一个大的文件分成若干块,一块一块的传输。这样做的好处可以减少重新上传的开销。比如:如果我们上传的文件是一个很大的文件,那么上传的时间应该会比较久,再加上网络不稳定各种因素的影响,很容易导致传输中断,用户除了重新上传文件外没有其他的办法,但是我们可以使用分片上传来解决这个问题。通过分片上传技术,如果网络传输中断,我们重新选择文件只需要传剩余的分片。而不需要重传整个文件,大大减少了重传的开销。

但是我们要如何选择一个合适的分片呢?因此我们要考虑如下几个事情:

​ 1.分片越小,那么请求肯定越多,开销就越大。因此不能设置太小。

​ 2.分片越大,灵活度就少了。

​ 3.服务器端都会有个固定大小的接收Buffer。分片的大小最好是这个值的整数倍。

图片概览

pP7NT4f.png

分片上传的步骤

1.先对文件进行md5加密。使用md5加密的优点是:可以对文件进行唯一标识,同样可以为后台进行文件完整性校验进行比对。

2.拿到md5值以后,服务器端查询下该文件是否已经上传过,如果已经上传过的话,就不用重新再上传。

3.对大文件进行分片。比如一个100M的文件,我们一个分片是5M的话,那么这个文件可以分20次上传。

4.向后台请求接口,接口里的数据就是我们已经上传过的文件块。(注意:为什么要发这个请求?就是为了能断点续传,比如我们使用百度网盘对吧,网盘里面有续传功能,当一个文件传到一半的时候,突然想下班不想上传了,那么服务器就应该记住我之前上传过的文件块,当我打开电脑重新上传的时候,那么它应该跳过我之前已经上传的文件块。再上传后续的块)。

5.开始对未上传过的文件块进行上传。(这个是第二个请求,会把所有的分片合并,然后上传请求)。

6.上传成功后,服务器会进行文件合并。最后完成。

接着讲讲具体的做法:

首先是跟后端约定好,每个分片是多大。

但是我们要如何选择一个合适的分片呢?因此我们要考虑如下几个事情:

1.分片越小,那么请求肯定越多,开销就越大。因此不能设置太小。

2.分片越大,灵活度就少了。

然后要判断文件大小,如果文件还没有一个分片大,那就直接走单文件直接上传的逻辑。否则就走分片上传的逻辑。我们约定的大小是5mb

首先将文件md5加密,获得加密的md5值,然后切片,(字节流)slice方法来切割的。切完片过后呢,开始走上传,这个时候我们做的秒传功能就体现出来了。在第一个分片里带上我们文件的md5值,后端判断,这个文件是否已经上传过,如果上传过,就直接返回一个标识符,就不用继续上传 ,直接秒传成功

假如没有,然后开始上传,上传使用的是并行上传。这里需要判断是并行上传还是串行上传。如果是串行上传的话,就对那个分片数组进行for循环,用async/await进行控制。如果是并行上传,就使用promise.allSettled来控制,这个api可以接收一个promise数组,然后并行执行里面的promise,然后返回一个结果数组,这个数组里面的每一项正好对应了那个promise数组里面的每一项promise的结果。

全部上传完成过后呢,会调用一个接口,在这个接口里后端会返回给我,他有哪些分片没有接收到,在我传给他的第一个分片中,已经告诉了他这个文件一共多少片,然后在上传每一片的时候,会带一个这一片是第几片的参数,也就是index,所以他能知道有哪些分片他没接收到。

如果真的有分片没有接收到。就得走续传的逻辑,这个时候我再重新上传,但是这次的重新上传,就只会上传上一次上传失败的那些分片,而不是全部重新上传。这次上传完过后,再去请求那个最后的接口,让后端告诉我他接收完了吗。如果接收完了,文件上传就结束了。如果没接收完。还是继续

话不多说,直接开始干代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
<!-- on-preview 点击文件列表中已上传的文件时的钩子 -->
<!-- http-request 覆盖默认的上传行为,可以自定义上传的实现 -->
<!-- limit 最大允许上传个数 -->
<!-- before-upload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 -->
<!-- accept 接受上传的文件类型(thumbnail-mode 模式下此参数无效) -->
<!-- multiple 是否支持多选文件 -->
<!-- on-change 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 -->
<!-- on-remove 文件列表移除文件时的钩子 -->
<!-- file-list 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] -->
<!-- on-exceed 文件超出个数限制时的钩子 -->
<!-- auto-upload 是否在选取文件后立即进行上传 -->
<!-- action 必选参数,上传的地址 例如 action="https://jsonplaceholder.typicode.com/posts/"-->
<el-upload
drag
multiple
:auto-upload="true"
:http-request="checkedFile"
:before-remove="removeFile"
:limit="10"
action="/tools/upload_test/"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
</el-upload>
<el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress>
</div>
</template>

文件上传时,会走http-request方法,如果定义了这个方法,组件的submit方法就会被拦截掉(注意别在这个方法里面调用组件的submit 方法,会造成死循环),在这个方法里面我就可以搞我想搞的事情了。

http-request 这个传入的回调函数应该返回一个Promise,所以我自己定义了一个无用的Promise进去

1
2
3
const prom = new Promise((resolve, reject) => {})
prom.abort = () => {}
return prom

如果要实现断点续传,需要和后端确定好,如何配合。

我这里的方案是,在我把所有的分片全部上传一遍后,会请求一个查询接口,后端在这个接口里面返回给我哪些分片没有上传成功(会给我序号),我这个时候,再去重新上传那些没有被上传成功的分片

直接贴完整代码,注释都在里面,看不懂的可以直接联系我,博客上有联系方式(依赖element-ui、axios、spark-md5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
<template>
<div>
<!-- on-preview 点击文件列表中已上传的文件时的钩子 -->
<!-- http-request 覆盖默认的上传行为,可以自定义上传的实现 -->
<!-- limit 最大允许上传个数 -->
<!-- before-upload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 -->
<!-- accept 接受上传的文件类型(thumbnail-mode 模式下此参数无效) -->
<!-- multiple 是否支持多选文件 -->
<!-- on-change 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 -->
<!-- on-remove 文件列表移除文件时的钩子 -->
<!-- file-list 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] -->
<!-- on-exceed 文件超出个数限制时的钩子 -->
<!-- auto-upload 是否在选取文件后立即进行上传 -->
<!-- action 必选参数,上传的地址 例如 action="https://jsonplaceholder.typicode.com/posts/"-->
<el-upload
drag
multiple
:auto-upload="true"
:http-request="checkedFile"
:before-remove="removeFile"
:limit="10"
action="/tools/upload_test/"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
</el-upload>
<el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress>
</div>
</template>
<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
export default {
data() {
return {
maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制 最小单位是b
multiUploadSize: 100 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传) 100mb
eachSize: 100 * 1024 * 1024, // 每块文件大小 100mb
requestCancelQueue: [], // 请求方法队列(调用取消上传
url: "/tools/upload_test/",
//上传进度
progress: 0,
showProgress: false,
// 每上传一块的进度
eachProgress: 0,
// 总共有多少块。断点续传使用
chunksKeep:0,
// 切割后的文件数组
fileChunksKeep:[],
// 这个文件,断点续传
fileKeep:null
};
},
mounted() {

},
methods: {
async checkedFile(options) {
console.log(options);
const {
maxSize,
multiUploadSize,
getSize,
splitUpload,
singleUpload
} = this; // 解构赋值
const { file, onProgress, onSuccess, onError } = options; // 解构赋值
if (file.size > maxSize) {
return this.$message({
message: `您选择的文件大于${getSize(maxSize)}`,
type: "error"
});
}
this.fileKeep = file
const uploadFunc =
file.size > multiUploadSize ? splitUpload : singleUpload; // 选择上传方式
try {
await uploadFunc(file, onProgress);
this.$message({
message: "上传成功",
type: "success"
});
this.showProgress = false;
this.progress = 0;
onSuccess();
} catch (e) {
console.error(e);
this.$message({
message: e.message,
type: "error"
});
this.showProgress = false;
this.progress = 0;
onError();
}
const prom = new Promise((resolve, reject) => {}); // 上传后返回一个promise
prom.abort = () => {};
return prom;
},
// 格式化文件大小显示文字
getSize(size) {
return size > 1024
? size / 1024 > 1024
? size / (1024 * 1024) > 1024
? (size / (1024 * 1024 * 1024)).toFixed(2) + "GB"
: (size / (1024 * 1024)).toFixed(2) + "MB"
: (size / 1024).toFixed(2) + "KB"
: size.toFixed(2) + "B";
},
// 单文件直接上传
async singleUpload(file, onProgress) {
await this.postFile(
{ file, uid: file.uid, fileName: file.fileName ,chunk:0},
onProgress
);
var spark = new SparkMD5.ArrayBuffer();
spark.append(file);
var md5 = spark.end();
console.log(md5);
const isValidate = await this.validateFile({
fileName: file.name,
uid: file.uid,
md5:md5,
chunks:1
});
},
// 大文件分块上传
splitUpload(file, onProgress) {
console.log('file11')
console.log(file)
return new Promise(async (resolve, reject) => {
try {
const { eachSize } = this;
const chunks = Math.ceil(file.size / eachSize);
this.chunksKeep = chunks
const fileChunks = await this.splitFile(file, eachSize, chunks);
this.fileChunksKeep = fileChunks
console.log('fileChunks,文件数组切割后')
console.log(fileChunks)
//判断每上传一个文件,进度条涨多少,保留两位小数

this.eachProgress = parseInt(Math.floor(100 / chunks * 100) / 100);

this.showProgress = true;
let currentChunk = 0;
for (let i = 0; i < fileChunks.length; i++) {
// 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传
console.log(currentChunk, i);
// 此时需要判断进度条

if (Number(currentChunk) === i) {
// 每块上传完后则返回需要提交的下一块的index
await this.postFile(
{
chunked: true,
chunk: i,
chunks,
eachSize,
fileName: file.name,
fullSize: file.size,
uid: file.uid,
file: fileChunks[i]
},
onProgress
);
currentChunk++

// 上传完一块后,进度条增加
this.progress += this.eachProgress;
// 不能超过100
this.progress = this.progress > 100 ? 100 : this.progress;
}
}
var spark = new SparkMD5.ArrayBuffer();
spark.append(file);
var md5 = spark.end();
console.log(md5);
const isValidate = await this.validateFile({
chunks: fileChunks.length,
// chunk: fileChunks.length,
fileName: file.name,
uid: file.uid,
md5:md5,
// task_id:file.uid
});
// if (!isValidate) {
// throw new Error("文件校验异常");
// }
resolve();
} catch (e) {
reject(e);
}
});
},
againSplitUpload(file, array) {
console.log('file,array')
console.log(file)
console.log(array)
return new Promise(async (resolve, reject) => {
try {
const { eachSize , fileKeep } = this;
const chunks = this.chunksKeep
const fileChunks = this.fileChunksKeep
this.showProgress = true;
// let currentChunk = 0;
for (let i = 0; i < array.length; i++) {
// 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传
// console.log(currentChunk, i);
// 此时需要判断进度条
// 每块上传完后则返回需要提交的下一块的index
await this.postFile(
{
chunked: true,
chunk: array[i],
chunks,
fileName: file.fileName,
fullSize: fileKeep.size,
uid: file.uid,
file: fileChunks[array[i]]
},
);
// currentChunk++

// 上传完一块后,进度条增加
// this.progress += this.eachProgress;
// 不能超过100
this.progress = this.progress > 100 ? 100 : this.progress;
}
var spark = new SparkMD5.ArrayBuffer();
spark.append(fileKeep);
var md5 = spark.end();
console.log(md5);
const isValidate = await this.validateFile({
chunks: fileChunks.length,
// chunk: fileChunks.length,
fileName: file.fileName,
uid: file.uid,
md5:md5,
// task_id:file.uid
});
// if (!isValidate) {
// throw new Error("文件校验异常");
// }
resolve();
} catch (e) {
reject(e);
}
});
},
// 文件分块,利用Array.prototype.slice方法
splitFile(file, eachSize, chunks) {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
const fileChunk = [];
for (let chunk = 0; chunks > 0; chunks--) {
fileChunk.push(file.slice(chunk, chunk + eachSize));
chunk += eachSize;
}
resolve(fileChunk);
}, 0);
} catch (e) {
console.error(e);
reject(new Error("文件切块发生错误"));
}
});
},
removeFile(file) {
this.requestCancelQueue[file.uid]();
delete this.requestCancelQueue[file.uid];
return true;
},
// 提交文件方法,将参数转换为FormData, 然后通过axios发起请求
postFile(param, onProgress) {
console.log(param);
const formData = new FormData();
// for (let p in param) {
// formData.append(p, param[p]);
// }
formData.append('file', param.file) // 改了
formData.append('uid',param.uid)
formData.append('chunk',param.chunk)
const { requestCancelQueue } = this;
const config = {
cancelToken: new axios.CancelToken(function executor(cancel) {
if (requestCancelQueue[param.uid]) {
requestCancelQueue[param.uid]();
delete requestCancelQueue[param.uid];
}
requestCancelQueue[param.uid] = cancel;
}),
onUploadProgress: e => {
if (param.chunked) {
e.percent = Number(
(
((param.chunk * (param.eachSize - 1) + e.loaded) /
param.fullSize) *
100
).toFixed(2)
);
} else {
e.percent = Number(((e.loaded / e.total) * 100).toFixed(2));
}
onProgress(e);
}
};
// return axios.post('/api/v1/tools/upload_test/', formData, config).then(rs => rs.data)
return this.$http({
url: "/tools/upload_test/",
method: "POST",

data: formData
// config
}).then(rs => rs.data);
},
// 文件校验方法
validateFile(file) {
// return axios.post('/api/v1/tools/upload_test/', file).then(rs => rs.data)
console.log(2)
console.log(file)
return this.$http({
url: "/tools/upload_test/upload_success/",
method: "POST",
data: file
}).then(res => {
if(res && res.status == 1){
this.againSplitUpload(file,res.data.error_file)
return true
}
});
}
}
};
</script>
<style scoped>
.progress{
/* 在当前页面居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 宽度 */
}
</style>

更新一波代码,上面的代码MD5加密后的值与后端不一致,换了个加密方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
<template>
<div :class="showProgress == true ? 'loading' : ''">
<!-- on-preview 点击文件列表中已上传的文件时的钩子 -->
<!-- http-request 覆盖默认的上传行为,可以自定义上传的实现 -->
<!-- limit 最大允许上传个数 -->
<!-- before-upload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 -->
<!-- accept 接受上传的文件类型(thumbnail-mode 模式下此参数无效) -->
<!-- multiple 是否支持多选文件 -->
<!-- on-change 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 -->
<!-- on-remove 文件列表移除文件时的钩子 -->
<!-- file-list 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] -->
<!-- on-exceed 文件超出个数限制时的钩子 -->
<!-- auto-upload 是否在选取文件后立即进行上传 -->
<!-- action 必选参数,上传的地址 例如 action="https://jsonplaceholder.typicode.com/posts/"-->
<el-upload drag multiple :auto-upload="true" :http-request="checkedFile" :before-remove="removeFile" :limit="10"
action="/tools/upload_chunk/" :disabled="showProgress">
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
</el-upload>
<!-- 正在上传的弹窗 -->
<el-dialog title="正在上传" :visible.sync="showProgress" width="50%">
<el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress>
</el-dialog>
<!-- <el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress> -->
</div>
</template>
<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
export default {
data() {
return {
maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制 最小单位是b
multiUploadSize: 100 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传) 100mb
eachSize: 100 * 1024 * 1024, // 每块文件大小 100mb
requestCancelQueue: [], // 请求方法队列(调用取消上传
url: "/tools/upload_chunk/",
//上传进度
progress: 0,
showProgress: false,
// 每上传一块的进度
eachProgress: 0,
// 总共有多少块。断点续传使用
chunksKeep: 0,
// 切割后的文件数组
fileChunksKeep: [],
// 这个文件,断点续传
fileKeep: null,
// 断点续传,文件md5
fileMd5Keep: ""
};
},
mounted() { },
methods: {
async checkedFile(options) {
// console.log(options);
const {
maxSize,
multiUploadSize,
getSize,
splitUpload,
singleUpload
} = this; // 解构赋值
const { file, onProgress, onSuccess, onError } = options; // 解构赋值
if (file.size > maxSize) {
return this.$message({
message: `您选择的文件大于${getSize(maxSize)}`,
type: "error"
});
}
this.fileKeep = file;
const uploadFunc =
file.size > multiUploadSize ? splitUpload : singleUpload; // 选择上传方式
try {
await uploadFunc(file, onProgress);
onSuccess();
} catch (e) {
console.error(e);
this.$message({
message: e.message,
type: "error"
});
this.showProgress = false;
this.progress = 0;
onError();
}
const prom = new Promise((resolve, reject) => { }); // 上传后返回一个promise
prom.abort = () => { };
return prom;
},
// 格式化文件大小显示文字
getSize(size) {
return size > 1024
? size / 1024 > 1024
? size / (1024 * 1024) > 1024
? (size / (1024 * 1024 * 1024)).toFixed(2) + "GB"
: (size / (1024 * 1024)).toFixed(2) + "MB"
: (size / 1024).toFixed(2) + "KB"
: size.toFixed(2) + "B";
},
// 单文件直接上传
async singleUpload(file, onProgress) {
await this.postFile(
{ file, uid: file.uid, fileName: file.fileName, chunk: 0 },
onProgress
);
// var spark = new SparkMD5.ArrayBuffer();
// spark.append(file);
// var md5 = spark.end();
// console.log(md5);

const reader = new FileReader();

reader.readAsArrayBuffer(file);
let hashMd5 = "";
console.log(hashMd5);
const that = this;
function getHash(cb) {
console.log("进入单个上传的getHash");
reader.onload = function (e) {
console.log("进入单个上传的getHash的函数2");
console.log(hashMd5);
console.log(this);
// console.log(e)
const hash = SparkMD5.ArrayBuffer.hash(e.target.result);
// const hash = SparkMD5.ArrayBuffer.hash(file);
console.log(hash);
that.hashMd5 = hash;
console.log(that.hashMd5);
that.fileMd5Keep = hash;
cb(hash);
};
}
await getHash(function (hash) {
console.log(hash);
console.log(that);
// 请求接口
that.validateFile({
name: file.name,
uid: file.uid,
md5: hash,
chunks: 1,
filter_type: "user_data_file"
});
});
},
// getMd5(file, chunkCount) {
// const spark = new SparkMD5.ArrayBuffer();
// let currentChunk = 0;

// const reader = new FileReader();

// reader.onload = function(e) {
// spark.append(e.target.result);
// currentChunk++;

// if (currentChunk < chunkCount) {
// console.log(currentChunk);
// loadNext();
// } else {
// console.log(spark.end());
// // 在这里请求接口
// return spark.end();
// }
// };

// function loadNext() {
// const start = currentChunk * chunkSize;
// const end =
// start + chunkSize >= file.size ? file.size : start + chunkSize;
// reader.readAsArrayBuffer(file.slice(start, end));
// }

// loadNext();
// },
// 大文件分块上传
splitUpload(file, onProgress) {
return new Promise(async (resolve, reject) => {
try {
const { eachSize } = this;
const chunks = Math.ceil(file.size / eachSize);
this.chunksKeep = chunks;
const fileChunks = await this.splitFile(file, eachSize, chunks);
this.fileChunksKeep = fileChunks;
console.log("fileChunks,文件数组切割后");
console.log(fileChunks);
//判断每上传一个文件,进度条涨多少,保留两位小数
this.eachProgress = parseInt(Math.floor((100 / chunks) * 100) / 100);
this.showProgress = true;
let currentChunk = 0;

for (let i = 0; i < fileChunks.length; i++) {
// 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传
console.log(currentChunk, i);
// 此时需要判断进度条
if (Number(currentChunk) === i) {
// 每块上传完后则返回需要提交的下一块的index
await this.postFile(
{
chunked: true,
chunk: i,
chunks,
eachSize,
fileName: file.name,
fullSize: file.size,
uid: file.uid,
file: fileChunks[i]
},
onProgress
);
currentChunk++;

// 上传完一块后,进度条增加
this.progress += this.eachProgress;
// 不能超过100
this.progress = this.progress > 100 ? 100 : this.progress;
}
}
// this.getMd5(file, chunks);
// var spark = new SparkMD5.ArrayBuffer();
// spark.append(file);
// var md5 = spark.end();
// console.log(md5);
const spark = new SparkMD5.ArrayBuffer();
let currentChunkMd5 = 0;
const that = this;
const reader = new FileReader();
reader.onload = async function (e) {
spark.append(e.target.result);
currentChunkMd5++;

if (currentChunkMd5 < chunks) {
loadNext();
} else {
// console.log(spark.end());
var hashMd5111 = spark.end();
that.fileMd5Keep = hashMd5111;
console.log(that);
console.log(hashMd5111);
// 在这里请求接口
await that.validateFile({
name: file.name,
uid: file.uid,
md5: hashMd5111,
chunks: fileChunks.length,
filter_type: "git_secret_file"
// chunk: fileChunks.length,
});
}
};

async function loadNext() {
const start = currentChunkMd5 * eachSize;
const end =
start + eachSize >= file.size ? file.size : start + eachSize;
await reader.readAsArrayBuffer(file.slice(start, end));
}
this.$message({
message: "正在进行文件加密校验",
type: "info"
});
await loadNext();
// let hashMd5 = "";
// // console.log(hashMd5)
// const that = this;
// console.log("进入分片上传的getHash");
// function getHash(cb) {
// reader.onload = function(e) {
// console.log("进入分片上传的getHash的函数");
// const hash = SparkMD5.ArrayBuffer.hash(e.target.result);
// // const hash = SparkMD5.ArrayBuffer.hash(file);
// console.log(hash);
// that.hashMd5 = hash;
// console.log(that.hashMd5);
// that.fileMd5Keep = hash;
// cb(hash);
// };
// reader.readAsArrayBuffer(file);
// }
// await getHash(function() {
// console.log(that);
// that.validateFile({
// name: file.name,
// uid: file.uid,
// md5: that.hashMd5,
// chunks: fileChunks.length
// // chunk: fileChunks.length,
// });
// });
// 请求接口

// console.log('fileChunks.length')
// 请求接口
// this.validateFile({
// fileName: file.name,
// uid: file.uid,
// md5:md5,
// chunks:1
// });
resolve();
} catch (error) {
reject(error);
}
});
},
// 断点续传
againSplitUpload(file, array) {
console.log("file,array");
console.log(file);
console.log(array);
return new Promise(async (resolve, reject) => {
try {
const { eachSize, fileKeep } = this;
const chunks = this.chunksKeep;
const fileChunks = this.fileChunksKeep;
this.showProgress = true;
// let currentChunk = 0;
for (let i = 0; i < array.length; i++) {
// 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传
// console.log(currentChunk, i);
// 此时需要判断进度条
// 每块上传完后则返回需要提交的下一块的index
await this.postFile({
chunked: true,
chunk: array[i],
chunks,
name: file.name,
fullSize: fileKeep.size,
uid: file.uid,
file: fileChunks[array[i]]
});
// currentChunk++

// 上传完一块后,进度条增加
// this.progress += this.eachProgress;
// 不能超过100
this.progress = this.progress > 100 ? 100 : this.progress;
}
// var spark = new SparkMD5.ArrayBuffer();
// spark.append(fileKeep);
// var md5 = spark.end();
// console.log(md5);

var fileMd5KeepTwo = this.fileMd5Keep;

const isValidate = await this.validateFile({
chunks: fileChunks.length,
// chunk: fileChunks.length,
name: file.name,
uid: file.uid,
md5: fileMd5KeepTwo,
filter_type: "git_secret_file"
// task_id:file.uid
});
// if (!isValidate) {
// throw new Error("文件校验异常");
// }
// 关闭进度条
this.showProgress = false;
// 重置进度条
this.progress = 0;
resolve();
} catch (e) {
reject(e);
}
});
},
// 文件分块,利用Array.prototype.slice方法
splitFile(file, eachSize, chunks) {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
const fileChunk = [];
for (let chunk = 0; chunks > 0; chunks--) {
fileChunk.push(file.slice(chunk, chunk + eachSize));
chunk += eachSize;
}
resolve(fileChunk);
}, 0);
} catch (e) {
console.error(e);
reject(new Error("文件切块发生错误"));
}
});
},
removeFile(file) {
this.requestCancelQueue[file.uid]();
delete this.requestCancelQueue[file.uid];
return true;
},
// 提交文件方法,将参数转换为FormData, 然后通过axios发起请求
postFile(param, onProgress) {
// console.log(param);
const formData = new FormData();
// for (let p in param) {
// formData.append(p, param[p]);
// }
formData.append("file", param.file); // 改了
formData.append("uid", param.uid);
formData.append("chunk", param.chunk);
formData.append("filter_type", "git_secret_file");
const { requestCancelQueue } = this;
const config = {
cancelToken: new axios.CancelToken(function executor(cancel) {
if (requestCancelQueue[param.uid]) {
requestCancelQueue[param.uid]();
delete requestCancelQueue[param.uid];
}
requestCancelQueue[param.uid] = cancel;
}),
onUploadProgress: e => {
if (param.chunked) {
e.percent = Number(
(
((param.chunk * (param.eachSize - 1) + e.loaded) /
param.fullSize) *
100
).toFixed(2)
);
} else {
e.percent = Number(((e.loaded / e.total) * 100).toFixed(2));
}
onProgress(e);
}
};
// return axios.post('/api/v1/tools/upload_chunk/', formData, config).then(rs => rs.data)
return this.$http({
url: "/tools/upload_chunk/",
method: "POST",

data: formData
// config
}).then(rs => rs.data);
},
// 文件校验方法
validateFile(file) {
// return axios.post('/api/v1/tools/upload_chunk/', file).then(rs => rs.data)
return this.$http({
url: "/tools/upload_chunk/upload_success/",
method: "POST",
data: file
}).then(res => {
if (res && res.status == 1) {
this.againSplitUpload(file, res.data.error_file);
this.$message({
message: "有文件上传失败,正在重新上传",
type: "warning"
});
} else if (res && res.status == 0) {
this.$message({
message: "上传成功",
type: "success"
});
this.showProgress = false;
this.progress = 0;
} else if (res && res.status == 40008) {
this.$message.error(res.message);
this.showProgress = false;
this.progress = 0;
}
});
}
}
};
</script>
<style scoped>
.loading {
/* 整体页面置灰 */
/* background: rgba(0, 0, 0, 0.5); */
}

.progress {
/* 在当前页面居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: 40px;
/* 宽度 */
}

/deep/ .el-dialog {
position: relative;
height: 500px;
}
</style>

问题解答

如果在上传途中断网中断了。怎么处理

通过localStorage记录切片上传的信息或者每次在上传切片前向后台询问该切片是否已经上传

暂停上传

请求一个接口,中断上传,(也就是中断promise.allSettled的外层循环)我自己这边和后端都能知道已经上传多少了。下一次就跳过已上传的就行

大文件分片上传的难点

加密,之前加密的值不一样,换了一种加密方法,

文件如何切片

并行上传,查了很多资料。

秒传,也是查资料,获得的

断点续传,根据那个失败的index数组重新上传,也可以让后端告诉我。最好是后端告诉我,这样最稳定,

暂停上传

如何分片,分成了什么

文件分块,利用Blob.prototype.slice方法

分成的是字节流,blob格式

上传应该是有顺序的,如何保证顺序的

其实都不用保证,因为我给了每次上传加了index,后端去排序就行

假如某个分片上传失败了,是怎么处理的

在promise.allSettled的失败数组里面有存储

后台如何知道是哪些分片没接收到

他可以通过index判断,第一个分片也告诉他总共多少分片了

怎么做并行上传

promise.allSettled

上传分片是怎么判断这个分片上传失败

promise报错就算失败,而且最后也是看后端告诉我哪些失败了

这几百个分片,如何同时上传一部分?

使用promise.allSettled,传递一个promise数组进去,返回的一个数组,对应的就是每个promise的响应

-------------本文结束感谢您的阅读-------------
技术原创:姚渐新,您的支持将鼓励我继续创作