vue+express实现大文件上传全流程
前言
逻辑流程
首先是整体流程,我搜了几篇文章,大体流程大同小异。都是,首先获取文件生成唯一的标识码,接着将文件分片处理,最后上传所有分片,当分片都传输成功之后通知后端去合并分片。
前端操作
获取文件的唯一编码hash值
这就需要引入一个包spark-md5
,它是基于md5的一种优秀的算法,用处很多,可以去计算文件的唯一身份证标识——hash值
。
calFileMd5Fn (chunks, progressCallbackFn) {
return new Promise((resolve, reject) => {
let currentChunk = 0
let spark = new SparkMD5.ArrayBuffer() // 实例化SparkMD5用于计算文件hash值
let fileReader = new FileReader() // 实例化文件阅读器用于读取blob二进制文件
fileReader.onerror = reject // 兜一下错
fileReader.onload = (e) => {
progressCallbackFn(Math.ceil(currentChunk / chunks.length * 100)) // 抛出一个函数,用于告知进度
spark.append(e.target.result) // 将二进制文件追加到spark中(官方方法)
currentChunk = currentChunk + 1 // 这个读完就加1,读取下一个blob
// 若未读取到最后一块,就继续读取;否则读取完成,Promise带出结果
if (currentChunk < chunks.length)
{
fileReader.readAsArrayBuffer(chunks[currentChunk])
} else
{
progressCallbackFn(100)
resolve(spark.end()) // resolve出去告知结果 spark.end官方api
}
}
// 文件读取器的readAsArrayBuffer方法开始读取文件,从blob数组中的第0项开始
fileReader.readAsArrayBuffer(chunks[currentChunk])
})
},
获取文件存储状态
当获取到hash值之后就需要去问下后端,我要的这个文件存不存在,文件完不完整啊。根据后端传的标识进行下一步操作。这里通常是出现3
种状态,1-文件存在、0-文件不存在、2-文件存在但是不完整
。后面会逐一讲述各种情况的处理方式。
if (res.status === 1)
{
// TODO: 文件已存在,直接100%(秒传)
this.fileProgress = 100
this.$message.success('文件上传成功')
} else if (res.status === 0)
{
//TODO: 文件不存在
filterChunks = this.fileNoneTurnFormData(res, chunks, file.name, hash)
this.uploadChunks(hash, file.raw.name, filterChunks, chunks)
} else if (res.status === 2)
{
//TODO: 文件存在,需要断点上传
filterChunks = this.breakPointTurnFormData(doneFileList, chunks, file.name, hash)
this.uploadChunks(hash, file.raw.name, filterChunks, chunks)
}
1-文件存在
当询问了后端得到文件存在的提示后,前端可以直接把文件传输进度拉满,做出一个秒上传的效果。当然,亦可以直接开启一个定时器几秒后上传完成,给进度条一个“上传”的效果。这块很简单,就不贴关键代码了。
0-文件不存在
当询问了后端得到文件不存在的提示后,前端就要把文件按前后端统一规定的大小进行分片处理。所谓分片,和字符串截取很相似,采用file
的api:file.slice
即可。
注意:file是特殊的二进制blob文件。
这时,就需要进行对每个请求传参进行封装。封装完成后逐一发送这些请求,最后通知后端把所有的分片给合并起来。
// 切割分片
sliceFn (file, chunkSize = 10 * 1024 * 1024) {
const result = [];
// 开始切割,一次切割1M
for (let i = 0; i < file.size; i = i + chunkSize)
{
result.push(file.slice(i, i + chunkSize));
}
return result;
},
// 上传分片
const requestList = formDataList.map(async (item, index) => {
console.log(item);
const res = await upload(item)
// 每上传完毕一片文件,后端告知已上传了多少片
this.fileProgress = Math.ceil((res.length / chunks.length) * 100)
return res
})
2-文件存在但不完整
当询问了后端得到文件不完整的提示后,这时候后端会传来一个数组,数组里包含存在的分片的名称,前端就需要先过滤一遍这些分片再组装请求参数,之后依旧是上传分片并且当上传完成之后去通知后端合并分片。
上传文件
上面已经说清了该流程。
后端操作
接口一:获取本地文件存储状态
第一个接口就是获取本地文件存储情况。这个很简单,file
库检测文件存在的状态即可完成。
const chunkDir = path.resolve(uploadFilesFolder, fileMd5)
if (!fse.existsSync(chunkDir)){
res.send(JSON.stringify({
status: 0,
message: '文件状态查询成功'
}))
return
} else {
// 如果有该目录,读取目录文件,若为空返回
fs.readdir(chunkDir, (err, data) => {
// 排除错误
if (err) throw new Error(err)
if (data.includes(fileName)){
// 文件未上传
res.send(JSON.stringify({
status: 1,
message: '文件状态查询成功'
}))
} else {
res.send(JSON.stringify({
status: 2,
doneFileList: data,
message: '文件状态查询成功'
}))
}
console.log("文件读取成功", data)// data => ['1','3.js']
})
}
接口二:接收分片
第二个接口是接收分片,这里有2个方案。
方案一,前端上传的文件会在后端本地有个缓存文件,可以直接读取这个文件,并把内容写入到目的目录中,然后删除缓存文件。
方案二,直接移动这个缓存文件到目录中,并按需更改文件名。
两种方案都可以完成,但是个人比较推荐使用方案二
,因为在本机和局域网联机实测的时候方案一在大量I/O处理的时候会有偶发性错误导致上传的文件损坏。
let readStream = fs.createReadStream(req.files.file.path), writeStream = fs.createWriteStream(chunkDir + '/' + req.body.chunkIndex);
readStream.pipe(writeStream);
readStream.on('end', function () {
fs.readdir(chunkDir, (err, data) => {
if (err) throw err
res.send()
})
})
接口三:合并分片
第三个接口,合并全部分片,这时需要先把所有分片排序,然后创建读取的数据流,和写入的数据流,把所有分片写入到一个文件中完成合并操作。
// 将切片转换成流进行合并
function pipeStream (path, writeStream) {
return new Promise(resolve => {
const readStream = fse.createReadStream(path)
readStream.on('end', () => {
fse.unlinkSync(path)
resolve('success')
})
readStream.pipe(writeStream)
})
}
额外功能
断点续传
这个地方在前端文件存在情况为2的时候即可得到体现。
断网重传/网络波动导致文件损坏
这个可以在上传的时候进行操作。
使用Promise.allSettled发请求,最后根据上传的情况进行处理。
检测每一片的上传情况,对于失败的片存入到localhost
,(考虑到上传过程中断网的情况发生),然后前端捕捉到进行信息提示,当再次发送文件的时候首先看看本地有没有这个失败分片,并根据文件hash值判断是不是本次要上传的文件。
这时,后端也需要进行对应的改变,接收参数的时候判断是否存在失败分片的数组,当存在的时候,首先删除目标目录下存在的失败的分片,之后返回对应的文件存储状态。
errChunks.map((item, index) => {
if(fse.existsSync(path.resolve(fileFolder, item))){
fse.unlinkSync(path.resolve(fileFolder, item))
}
})
性能实测
声明:以下数据为多次实验的平均值,同时实验环境受机器配置、网络情况等多重因素影响,数据仅供参考。
分片大小 | 文件大小 | 上传时间 | 具体各种时间 | |
---|---|---|---|---|
本地 | 10M | 约5GB | 约2分钟 | 生成hash值约1分钟、上传约1分钟、合并文件约1分钟 |
局域网联机 | 10M | 约5GB | 约14分钟 | 生成hash值约1分钟、上传约9分钟、合并文件约3分钟 |
总结
本次使用vue
搭前端,express
搭node
后端。暂未支持多文件上传,同时,获取文件hash值的操作可以开启辅助线程来优化,详细将下面的参考资料。同时当多次上传的时候,会有偶发性上传的文件损坏的情况,可能存在隐性bug未被发现,开发不易,但我会继续学习持续优化~
代码仓库
代码仓库:qianchunzhao322/bigFileUpload (github.com) 欢迎大家来点亮星星……
参考资料
转载自:https://juejin.cn/post/7239953755898134585