根据部门的业务需求,需要在网络状态不良的情况下上传很大的文件(1G+)。
其中会遇到的问题:
1,文件过大,超出服务端的请求大小限制;
2,请求时间过长,请求超时;
3,传输中断,必须重新上传导致前功尽弃。
解决方案实现思路,拿到文件,保存文件唯一性标识,切割文件、分片上传、文件MD5验证、断点续传、手动重试上传。
鉴于过往有使用过webupload文件上传组件的经验,于是此次采用的是Plupload作为替换。Plupload是一款由著名的web编辑器TinyMCE团队开发的上传组件,简单易用且功能强大。
Plupload有以下功能和特点
- 拥有多种上传方式:HTML5、flash、silverlight以及传统的
<input type=”file” />
。Plupload会自动侦测当前的环境,选择最合适的上传方式,并且会优先使用HTML5的方式。所以你完全不用去操心当前的浏览器支持哪些上传方式,Plupload会自动为你选择最合适的方式。 - 支持以拖拽的方式来选取要上传的文件
- 支持在前端压缩图片,即在图片文件还未上传之前就对它进行压缩
- 可以直接读取原生的文件数据,这样的好处就是例如可以在图片文件还未上传之前就能把它显示在页面上预览
- 支持把大文件切割成小片进行上传,因为有些浏览器对很大的文件比如几G的一些文件无法上传。
环境
- vue2.x
- webpack3.x
- axios
代码
npm安装plupload,文件引入组件,
1
2
3
4
5
6
7
8
9
10
11
12
|
<uploader browse_button=
"upload_area"
:max_retries=
"3"
:url=
"action"
:headers=
"headers"
chunk_size=
"10MB"
drop_element=
"upload_area"
@disableBrowse=
"!loading"
:BeforeUpload=
"beforeUpload"
:ChunkUploaded=
"chunkUploaded"
:FilesAdded=
"filesAdded"
:StateChanged=
"stateChanged"
@inputUploader=
"inputUploader"
/>
|
初始化方法filesAdded(),每次上传前清空队列的其他文件,保证上传的一致性。其次对文件类型进行判断过滤fileType(),文件进入时进行总md5一次fileMd5(),然后进入文件分片chunkCheckStatus(),每个分片都要进行md5并与后台进行校验fileMd5(),确保文件在中断后继续上传的准确性。
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
|
filesAdded (up, files) {
// 删除上传队列中其他文件,只保留最近上传的文件
let
fileLen = files.length, that =
this
if
(fileLen > 1) {
files = files.splice(0, fileLen - 1)
// 清空上传队列
}
files.forEach((f) => {
f.status = -1
that.dataForm.file = f
that.fileType(f.getNative())
if
(that.loading) {
that.computeStatus =
true
that.progress = 0
// 文件分片
let
chunkSize = 2097152,
// Read in chunks of 2MB
chunks = Math.ceil(f.size / chunkSize)
that.fileMd5(f.getNative(), (e, md5) => {
that.dataForm.md5 = md5
if
(that.loading ==
true
) {
that.count = 0
that.chunkCheckStatus(md5, that.dataForm.fileName, (uploader, dataList) => {
that.uploading = uploader
if
(that.uploading ==
true
) {
for
(
let
chunk = 1; chunk <= chunks; chunk++) {
that.fileChunkFile(f.getNative(), chunk, (e, chunkFile) => {
that.fileMd5(chunkFile, (e, blockMd5) => {
that.PostFile(up, chunkFile, chunk, chunks, md5, blockMd5)
})
})
}
}
else
{
// 去重
that.progress = 0
for
(
let
chunk = 1; chunk <= chunks; chunk++) {
let
status = 0
dataList.some((item) => {
if
(item.chunk == chunk) {
status = 1
return
false
}
})
if
(status == 0) {
that.fileChunkFile(f.getNative(), chunk, (e, chunkFile) => {
that.fileMd5(chunkFile, (e, blockMd5) => {
that.PostFile(up, chunkFile, chunk, chunks, md5, blockMd5)
})
})
}
}
}
})
}
})
}
})
}
|
文件md5方法,这里使用了SparkMD5,import SparkMD5 from '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
|
fileMd5 (file, callback) {
let
that =
this
var
blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
file = file,
chunkSize = 2097152,
// Read in chunks of 2MB
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark =
new
SparkMD5.ArrayBuffer(),
fileReader =
new
FileReader()
fileReader.onload =
function
(e) {
console.log(
'read chunk nr'
, currentChunk + 1,
'of'
, chunks)
spark.append(e.target.result)
// Append array buffer
currentChunk++
if
(currentChunk < chunks) {
loadNext()
}
else
{
let
blockMd5 =
''
blockMd5 = spark.end()
callback(
null
, blockMd5)
}
}
fileReader.onerror =
function
() {
callback(
'oops, something went wrong.'
)
}
function
loadNext () {
var
start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
}
loadNext()
}
|
文件分片上传方法,验证总分片信息后,把每个分片进行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
|
chunkCheckStatus (md5, fileName, callback) {
this
.$http({
url:
this
.$http.adornUrl(
'/biz/upload/getFileBlockStatus'
),
method:
'get'
,
params:
this
.$http.adornParams({
md5: md5,
fileName: fileName
})
}).then(({ data }) => {
if
(data && data.code === 0) {
if
(data.list !=
null
) {
this
.uploading =
false
this
.chunkCheckData = []
data.list.map((item, index) => {
if
(item.isUpload ==
true
) {
this
.count++
this
.chunkCheckData.push(item)
}
})
callback(
this
.uploading,
this
.chunkCheckData)
return
}
this
.uploading =
true
callback(
this
.uploading)
}
else
{
this
.$message.error(data.msg)
this
.loading =
false
this
.computeStatus =
false
return
false
}
})
}
|
详细的配置信息可以参考我写的这篇文章:http://blog.ncmem.com/wordpress/2019/08/09/vue%e5%a4%a7%e6%96%87%e4%bb%b6%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/