目录
  • 1、整体思路
  • 2、实现步骤
    • 2.1 文件切片加密
    • 2.2 查询上传文件状态
    • 2.3 秒传
    • 2.4 上传分片、断点续传
    • 2.5 合成分片还原完整文件
  • 3、总结
    • 4、后续扩展与思考
      • 5、源码

        1、整体思路

        • 将文件切成多个小的文件;
        • 将切片并行上传;
        • 所有切片上传完成后,服务器端进行切片合成;
        • 当分片上传失败,可以在重新上传时进行判断,只上传上次失败的部分实现断点续传;
        • 当切片合成为完整的文件,通知客户端上传成功;
        • 已经传到服务器的完整文件,则不需要重新上传到服务器,实现秒传功能;

        2、实现步骤

        2.1 文件切片加密

        利用MD5 , MD5 是文件的唯一标识,可以利用文件的 MD5 查询文件的上传状态;

        读取进度条进度,生成MD5:

        React+Node实现大文件分片上传、断点续传秒传思路

        实现结果:

        React+Node实现大文件分片上传、断点续传秒传思路

        实现代码如下:

        const md5File = (file) => {
            return new Promise((resolve, reject) => {
              // 文件截取
              let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
                chunkSize = file?.size / 100,
                chunks = 100,
                currentChunk = 0,
                spark = new SparkMD5.ArrayBuffer(),
                fileReader = new FileReader();
        
              fileReader.onload = function (e) {
                console.log('read chunk nr', currentChunk + 1, 'of', chunks);
                spark.append(e.target.result);
                currentChunk += 1;
        
                if (currentChunk < chunks) {
                  loadNext();
                } else {
                  let result = spark.end()
                  resolve(result)
                }
              };
        
              fileReader.onerror = function () {
                message.error('文件读取错误')
              };
        
              const loadNext = () => {
                const start = currentChunk * chunkSize,
                  end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
        
                // 文件切片
                fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
                // 检查进度条
                dispatch({ type: 'check', checkPercent: currentChunk + 1 })
              }
        
              loadNext();
            })
          }

        2.2 查询上传文件状态

        利用当前md5去查询服务器创建的md5文件夹是否存在,如果存在则返回该目录下的所有分片;

        React+Node实现大文件分片上传、断点续传秒传思路

        前端只需要拿MD5和文件名去请求后端,这里就不在列出来;
        node端代码逻辑:

        app.get('/check/file', (req, resp) => {
          let query = req.query
          let fileName = query.fileName
          let fileMd5Value = query.fileMd5Value
          // 获取文件Chunk列表
          getChunkList(
              path.join(uploadDir, fileName),
              path.join(uploadDir, fileMd5Value),
              data => {
                  resp.send(data)
              }
          )
        })
        
        // 获取文件Chunk列表
        async function getChunkList(filePath, folderPath, callback) {
          let isFileExit = await isExist(filePath)
          let result = {}
          // 如果文件已在存在, 不用再继续上传, 真接秒传
          if (isFileExit) {
              result = {
                  stat: 1,
                  file: {
                      isExist: true,
                      name: filePath
                  },
                  desc: 'file is exist'
              }
          } else {
              let isFolderExist = await isExist(folderPath)
              // 如果文件夹(md5值后的文件)存在, 就获取已经上传的块
              let fileList = []
              if (isFolderExist) {
                  fileList = await listDir(folderPath)
              }
              result = {
                  stat: 1,
                  chunkList: fileList,
                  desc: 'folder list'
              }
          }
          callback(result)
        }

        2.3 秒传

        如果上传的当前文件已经存在服务器目录,则秒传;

        服务器端代码已给出,前端根据返回的接口做判断;

        if (data?.file) {
          message.success('文件已秒传')
          return
        }

        实现效果:

        React+Node实现大文件分片上传、断点续传秒传思路

        2.4 上传分片、断点续传

        检查本地切片和服务器对应的切片,如果没有当前切片则上传,实现断点续传;
        同步并发上传所有的切片,维护上传进度条状态;
        前端代码:

        /**
           * 上传chunk
           * @param {*} fileMd5Value 
           * @param {*} chunkList 
           */
          async function checkAndUploadChunk(file, fileMd5Value, chunkList) {
            let chunks = Math.ceil(file.size / chunkSize)
            const requestList = []
            for (let i = 0; i < chunks; i++) {
              let exit = chunkList.indexOf(i + "") > -1
              // 如果不存在,则上传
              if (!exit) {
                requestList.push(upload({ i, file, fileMd5Value, chunks }))
              }
            }
        
            // 并发上传
            if (requestList?.length) {
              await Promise.all(requestList)
            }
          }
        
            // 上传chunk
          function upload({ i, file, fileMd5Value, chunks }) {
            current = 0
            //构造一个表单,FormData是HTML5新增的
            let end = (i + 1) * chunkSize >= file.size ? file.size : (i + 1) * chunkSize
            let form = new FormData()
            form.append("data", file.slice(i * chunkSize, end)) //file对象的slice方法用于切出文件的一部分
            form.append("total", chunks) //总片数
            form.append("index", i) //当前是第几片     
            form.append("fileMd5Value", fileMd5Value)
            return axios({
              method: 'post',
              url: BaseUrl + "/upload",
              data: form
            }).then(({ data }) => {
              if (data.stat) {
                current = current + 1
                const uploadPercent = Math.ceil((current / chunks) * 100)
                dispatch({ type: 'upload', uploadPercent })
              }
            })
          }

        Node端代码:

        app.all('/upload', (req, resp) => {
          const form = new formidable.IncomingForm({
              uploadDir: 'nodeServer/tmp'
          })
          form.parse(req, function(err, fields, file) {
              let index = fields.index
              let fileMd5Value = fields.fileMd5Value
              let folder = path.resolve(__dirname, 'nodeServer/uploads', fileMd5Value)
              folderIsExit(folder).then(val => {
                  let destFile = path.resolve(folder, fields.index)
                  copyFile(file.data.path, destFile).then(
                      successLog => {
                          resp.send({
                              stat: 1,
                              desc: index
                          })
                      },
                      errorLog => {
                          resp.send({
                              stat: 0,
                              desc: 'Error'
                          })
                      }
                  )
              })
          })

        实现效果:

        React+Node实现大文件分片上传、断点续传秒传思路

        存储形式:

        React+Node实现大文件分片上传、断点续传秒传思路

        2.5 合成分片还原完整文件

        当所有的分片上传完成,前端通知服务器端分片上传完成,准备合成;

        前端代码:

          /**
           * 所有的分片上传完成,准备合成
           * @param {*} file 
           * @param {*} fileMd5Value 
           */
          function notifyServer(file, fileMd5Value) {
            let url = BaseUrl + '/merge?md5=' + fileMd5Value + "&fileName=" + file.name + "&size=" + file.size
            axios.get(url).then(({ data }) => {
              if (data.stat) {
                message.success('上传成功')
              } else {
                message.error('上传失败')
              }
            })
          }

        Node端代码:

        // 合成
        app.all('/merge', (req, resp) => {
          let query = req.query
          let md5 = query.md5
          let fileName = query.fileName
          console.log(md5, fileName)
          mergeFiles(path.join(uploadDir, md5), uploadDir, fileName)
          resp.send({
              stat: 1
          })
        })
        
        
        // 合并文件
        async function mergeFiles(srcDir, targetDir, newFileName) {
          let fileArr = await listDir(srcDir)
          fileArr.sort((x,y) => {
              return x-y;
          })
          // 把文件名加上文件夹的前缀
          for (let i = 0; i < fileArr.length; i++) {
              fileArr[i] = srcDir + '/' + fileArr[i]
          }
          concat(fileArr, path.join(targetDir, newFileName), () => {
              console.log('合成成功!')
          })
        }

        请求实现:

        React+Node实现大文件分片上传、断点续传秒传思路

        合成文件效果:

        React+Node实现大文件分片上传、断点续传秒传思路

        3、总结

        将文件切片,并发上传切片,切片合成完整文件,实现分片上传;
        使用MD5标识文件夹,得到唯一标识;
        分片上传前通过文件 MD5 查询已上传切片列表,上传时只上传未上传过的切片,实现断点续传;
        检查当前上传文件,如果已存在服务器,则不需要再次上传,实现秒传;

        4、后续扩展与思考

        使用时间切片计算hash

        当文件过大时需要计算很久的hash,页面不能做其他的操作,所以考虑使用React-Fiber的架构理念,利用浏览器空闲时间去计算hash。考虑使用window.requestIdleCallback()函数;

        请求并发控制

        假如一个文件过大,就会切割成许多的碎片,一次性发几百个请求,这显然是不行的;所以要考虑请求并发数控制;

        5、源码

        地址:https://github.com/linhexs/file-upload.git

        声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。