大文件分片上传
在实际生产中,上传超过 5M 的文件,就会有一定网络风险,一般建议采用分片上传
大文件分片上传是指将一个大文件分成若干个小块,分别上传到服务器,最后合并成一个完整的文件。
这种方式可以有效地避免上传过程中出现网络中断、服务器宕机等情况导致上传失败的情况。
大文件分片上传的好处在于:
- 减少上传失败的可能性。由于将一个大文件分成多个小块进行上传,如果其中某一个小块上传失败,只需要重新上传该小块即可,不需要重新上传整个文件。
- 加快上传速度。由于将一个大文件分成多个小块上传,可以同时上传多个小块,从而提高上传速度。
# 原理
分片上传需要解决的核心问题是:怎么分片才能确保文件的完整性
这里我们采用一种简单的文件名+md5 来实现
实际生产中一般会用哈希来命名切片,这里为了简便我们直接文件名+后缀即可
# 代码实现
前端代码
const sliceSize = 5 * 1024 * 1024 // 每个文件切片大小定为5MB
//发送请求
function upload() {
const blob = document.getElementById('file').files[0]
const fileSize = blob.size // 文件大小
const fileName = blob.name // 文件名
//计算文件切片总数
const totalSlice = Math.ceil(fileSize / sliceSize)
// 循环上传
for (let i = 1; i <= totalSlice; i++) {
let chunk
if (i == totalSlice) {
// 最后一片
chunk = blob.slice((i - 1) * sliceSize, fileSize - 1) //切割文件
} else {
chunk = blob.slice((i - 1) * sliceSize, i * sliceSize)
}
let chunkName = `${fileName}-${totalSlice}`
const formData = new FormData()
formData.append('file', chunk)
formData.append('md5', md5(blob))
formData.append('name', chunkName)
formData.append('size', fileSize)
formData.append('chunks', totalSlice)
formData.append('chunk', i)
$.ajax({
url: '/chunk/upload',
type: 'POST',
cache: false,
data: formData,
processData: false,
contentType: false,
async: false
})
}
}
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
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
前端在上传完后,后端将所有切片存储在一个文件夹内
这时前端告诉后端上传完毕,后端将分片合并为一个完整文件
function mergeChunks(size = 5 * 1024 * 1024) {
$.ajax({
url: '/merge',
headers: {
'content-type': 'application/json'
},
data: JSON.stringify({
fileName: fileName.name
})
})
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
后端处理后
后端合并文件并校验 md5 是否匹配以保证文件完整性
# 如何实现断点续传
比如一个文件被切成 10 片,当你上传成功 5 片后,突然暂停,那么下次点击续传时,只需要过滤掉之前已经上传成功的那 5 片就行,怎么实现呢?
实现方式有多种,这里我们选一种简单方式:
前端请求接口,后端返回切片文件夹里现在已成功上传的切片名列表,然后前端过滤后再把还未上传的切片的继续上传就行了
后端 node 代码类似
function verify() {
// 返回已经上传切片名列表
const createUploadedList = async (fileName) =>
fse.existsSync(path.resolve(UPLOAD_DIR, fileName))
? await fse.readdir(path.resolve(UPLOAD_DIR, fileName))
: []
const data = await resolvePost(req)
const { fileName } = data
const filePath = path.resolve(UPLOAD_DIR, fileName)
console.log(filePath)
if (fse.existsSync(filePath)) {
res.end(
JSON.stringify({
shouldUpload: false
})
)
} else {
res.end(
JSON.stringify({
shouldUpload: true,
uploadedList: await createUploadedList(fileName)
})
)
}
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
前端代码,upload 加入判断是否上传过的逻辑
async function keepUpload() {
const { uploadedList } = await this.verifyUpload(fileName)
upload(uploadedList)
}
function upload(uploadedList) {
const blob = document.getElementById('file').files[0]
const fileSize = blob.size // 文件大小
const fileName = blob.name // 文件名
//计算文件切片总数
const totalSlice = Math.ceil(fileSize / sliceSize)
// 循环上传
for (let i = 1; i <= totalSlice; i++) {
let chunk
if (i == totalSlice) {
// 最后一片
chunk = blob.slice((i - 1) * sliceSize, fileSize - 1) //切割文件
} else {
chunk = blob.slice((i - 1) * sliceSize, i * sliceSize)
}
let chunkName = `${fileName}-${totalSlice}`
// 如果切片上传过了 跳过
if (uploadedList.includes(chunkName)) continue
const formData = new FormData()
formData.append('file', chunk)
formData.append('md5', md5(blob))
formData.append('name', chunkName)
formData.append('size', fileSize)
formData.append('chunks', totalSlice)
formData.append('chunk', i)
if (uploadedList)
$.ajax({
url: '/chunk/upload',
type: 'POST',
cache: false,
data: formData,
processData: false,
contentType: false,
async: false
})
}
}
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
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
# 总结
上面是一个大文件分片上传的简单 demo,实际生产中的设计会复杂的多,可以思考下面几个问题
- 前后端如何配合设计切片名和文件名保证唯一性
- 前端实现上传进度条 (利用 ajax onUploadProgress 监听)
- 文件合并后 md5 匹配不上的复传机制
上次更新: 2023/04/05, 09:41:10