在Web开发中,处理大文件上传是一个常见的需求。为了提高上传效率和用户体验,我们可以采用文件切片的方式,将大文件切割成多个小文件片段(或称为“块”或“chunk”),然后并行上传到服务器。以下是如何使用JavaScript的Promise来实现这一功能的分享笔记。
功能关键字
1、利用Promise.race + Promise.all + 递归控制最大并发为6个;
2、错误中断处理;
3、上传试探;
4、上传进度展示
实现步骤
1. 文件切片
首先,使用JavaScript的File API对大文件进行切片。File.slice()方法可以用来将文件切割成多个小片段。对每个切片,附带必要的信息,如切片的索引、文件总大小、分片总数等。
// 将文件分块
chunkFile(file, chunkSize = 1024 * 1024 * 5) { // 切片大小5MB
const chunks = []
const count = Math.ceil(file.size / chunkSize)
for (let i = 0; i < count; i++) {
const start = i * chunkSize
const end = Math.min(file.size, start + chunkSize)
// 切片文件
const chunk = file.slice(start, end)
chunks.push({ number: i + 1, data: chunk })
}
return chunks
}
2. 创建上传逻辑
接下来,为这些切片文件创建上传逻辑。使用fetch或XMLHttpRequest发送POST请求,将每个切片作为请求体发送到服务器。
// 切割包上传
uploadChunk(chunk, currentIndex) {
return new Promise(async(resolve, reject) => {
// 模拟上传
setTimeout(() => {
if (Math.random() > 0.2) {
console.warn('上传:切片number' + chunk.number + '|切片角标:' + currentIndex)
resolve(`Chunk ${chunk.number} uploaded successfully`)
} else {
reject(`Chunk ${chunk.number} upload failed`)
}
}, 1000)
})
}
3、使用Promise控制并发 Promise.race + Promise.all + 递归
1、限制并行上传的切片数量(比如最多6个),可以通过Promise数组结合递归控制并发数量,通过Promise.race监听第一个上传成功,并及时补充切片,Promise.all监听所有切片上传完成; 2、监听每个上传请求的进度事件和错误事件。当切片上传成功时,可以移除对应的切片数据,并追加新的上传;上传失败时,终止后续上传。
/**
* 分片上传所有文件
* @param chunks 分片对象
* @returns promise
*/
uploadLeftChunks(chunks) {
console.log('总切片数:' + chunks.length)
return new Promise((resolve, reject) => {
// 控制最大并发,减小服务器压力
const uploadFileChunksInParallel = (chunks, maxConcurrent = 6) => {
let running = 0
const promises = []
let currentIndex = 0
// 一旦有一个报错了,就不继续启动后面的上传了
let ifFailed = false
// 递归函数,用于启动或等待新的上传
const startUpload = (index) => {
currentIndex = index
if (currentIndex >= chunks.length || ifFailed) {
console.log('所有切片都上传完毕或有切片上传失败, 停止上传')
// 所有切片都上传完毕
return resolve(Promise.all(promises))
}
if (running < maxConcurrent) {
console.log('小于最大并行数,继续插入请求' + currentIndex)
console.log('当前切片:' + currentIndex)
// 如果当前运行的上传数量小于最大并发数,则启动一个新的上传
running++
const promise = this.uploadChunk(chunks[currentIndex], currentIndex)
.then(result => {
console.log('切片上传成功')
running--
// 启动下一个上传
startUpload(currentIndex + 1)
})
.catch(error => {
console.log('请求失败一个,任务终止')
ifFailed = true
reject(error)
})
promises.push(promise)
} else {
console.log('等于最大并行数,等待第一个请求完成插入请求')
// 否则,等待一个Promise完成后再启动新的上传
Promise.race(promises).then(() => {
startUpload(currentIndex)
})
}
}
// maxConcurrent, chunks.length取最小进行调用
const times = Math.min(maxConcurrent, chunks.length)
for (let i = 0; i < times; i++) {
// 一次性启动启动上传
startUpload(i)
}
}
uploadFileChunksInParallel(chunks, 6)
})
}
4. 上传试探 + 上传完成后合并
上传试探的作用是确保文件类型符合要求,接口能正常请求,避免直接平铺请求导致的过多无效请求。当所有切片都上传完毕后,发送一个结束上传的请求给服务器,告知服务器所有切片已经上传完成,可以进行文件的重组。
// 试探上传第一块
await this.uploadChunk(chunks[0], 1)
let uploadRes
if (chunks.length > 1) {
// 上传剩下的所有的块
uploadRes = await this.uploadLeftChunks(chunks.slice(1))
}
// 处理所有块上传成功后的逻辑,例如合并文件块等
console.log('All chunks uploaded successfully:', uploadRes)
const { data: { path: completeUploadRes }} = await this.completeUpload()
6. 进度显示
在整个上传过程中,可以根据每个切片的上传进度来更新整体上传进度,并展示给用户。
// 通过当前完成的任务数 / 总任务数:切片上传任务总数 + 1(合并请求任务)得到进度百分比
finishedTaskCount = 20
totalTaskCount = 200
总结
通过上述步骤,我们可以使用Promise实现大文件切片的并行上传,并控制同时上传的切片数量,以提高上传效率和用户体验。在实际应用中,还需要考虑错误处理、重试机制、上传进度显示等细节。