likes
comments
collection
share

vue+express实现大文件上传全流程

作者站长头像
站长
· 阅读数 38

前言

逻辑流程

首先是整体流程,我搜了几篇文章,大体流程大同小异。都是,首先获取文件生成唯一的标识码,接着将文件分片处理,最后上传所有分片,当分片都传输成功之后通知后端去合并分片。

前端操作

获取文件的唯一编码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搭前端,expressnode后端。暂未支持多文件上传,同时,获取文件hash值的操作可以开启辅助线程来优化,详细将下面的参考资料。同时当多次上传的时候,会有偶发性上传的文件损坏的情况,可能存在隐性bug未被发现,开发不易,但我会继续学习持续优化~

代码仓库

代码仓库:qianchunzhao322/bigFileUpload (github.com) 欢迎大家来点亮星星……

参考资料

转载自:https://juejin.cn/post/7239953755898134585
评论
请登录