场景及概述
前端web应用上传大文件,经常遇到文件上传途中用户不小心关闭了页面、网络中断等情况,文件分片上传➕断点续传能够将大文件分片发送多个分片上传请求,在遇到上传中断等情况时,可以根据服务器返回的已有分片筛选未上传的分片进行上传,减少服务器的存储压力,本文将实现一个具备以上功能的文件上传类
功能及整体流程设计
该类具备基本文件上传功能,此外能够对文件分片、在上传前将文件信息发送给服务器确定文件在服务器的信息后确定上传分片序号进行上传、中断上传。这里使用md5进行文件摘要,整体流程设计如下
关键步骤解析
1.类初始化
使用外部传入的参数初始化类,option中含有待上传的文件对象,并初始化所有变量,waitUploadChunks是根据allChunks和uploadedChunks计算出来的上传分片,fileMd5用于发送给后端确认该文件是否已经完整上传到服务器
constructor(options) {
// 初始化配置
this.options = options
// 默认的单个分片大小
this.defaultChunkSize = 1024 * 1024 * 10
// 默认的上传地址
this.defaultApi = '/api/file-cloud-web-server/sharding-file/upload'
// 文件所有分片序列
this.allChunks = []
// 已上传成功的分片序列
this.uploadedChunks = []
// 等待上传的分片序列
this.waitUploadChunks = []
// 文件MD5
this.fileMD5 = ''
// 最大上传进程数 默认同时上传5个
this.maxThreads = 5
this.uploadStatus = {
preparing: 'preparing',
uploading: 'uploading',
success: 'success',
failed: 'failed',
}
// 上传分片集合
this.uploadPieces = []
// 上传进度
this.uploadProgress = 0
}
2.整体流程控制
set_start方法确定整个上传的整体流程,计算文件md5-获取文件在服务器的位置信息-修改文件上传状态-加载文件分片上传
set_start() {
this.getFileMD5(() => {
this.getFileInfo(() => {
this.completed(this.uploadStatus.uploading)
this.loadNextChunk()
})
})
}
3. 计算文件md5并获取文件在服务器的信息
readNext方法中对文件进行分片,同时使用fileReader 和 sparkMd5对文件计算md5,后续通过请求后端接口,将需要上传的分片序号放在waitUploadChunks 中
getFileMD5(callback) {
let chunkNumber = 0
const spark = new SparkMD5.ArrayBuffer()
const reader = new FileReader()
// 使用reader将文件分片并读入
// onload 为reader读取完分片后的操作
reader.onload = (e) => {
spark.append(e.target.result)
chunkNumber++
if (chunkNumber < this.totalChunks) {
this.readNext(chunkNumber, reader)
} else {
// 已经将所有分片读取完成
this.fileMD5 = spark.end()
console.log(`文件MD5:${
this.fileMD5}`)
// 下一步:获取文件在服务器的位置
callback()
}
}
this.readNext(chunkNumber, reader)
}
4. 上传分片
使用formData上传分片,用axios的CancelToken 获取中断该分片上传的方法;此处需要注意的是文件上传进度的计算,由于文件被分成了多个分片上传,请求的字节流包括的多个上传请求的请求头,因此每个请求的字节流之和会大于原文件的实际大小,在这里我使用的单个分片的上传进度累加获得整个文件的上传进度
uploadFileBinary(chunkNumber, filePath) {
const url = this.options.url || this.defaultApi
//声明一个formdata对象,用于存储file文件以及其他需要传递给服务器的参数
const formData = new FormData()
formData.append('chunkNumber', chunkNumber)
formData.append('totalChunks', this.totalChunks)
formData.append('identifier', this.fileMD5)
formData.append('totalSize', this.options.file.fileObj.size)
formData.append('filename', this.options.file.fileObj.name || '')
formData.append('file', this.allChunks[chunkNumber])
// 中断请求准备
const CancelToken = axios.CancelToken //获得Cancel对象
const that = this
axios({
url,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 18000000,
data: formData,
onUploadProgress: (res) => {
// 计算每个分片的占比
const rate = 100 / this.total