大文件切片上传优化,子线程计算文件hash,pLimit库并发控制上传

生成hash

无论是客户端还是服务端,都要用到文件和切片的 hash,生成 hash 最简单的方法是 文件名 + 切片下标,但是如果文件名一旦修改,生成的 hash 就会失效。事实上只要文件内容不变, hash 就不应该变化,所以我们根据文件内容生成 hash。

这里我们选用 spark-md5库,它可以根据文件内容计算出文件的hash值。

代码语言:javascript
复制
imort SparkMD5 from 'spark-md5.min.js'
/**
 * 生成文件hash
 */
const chunkHash = async () => {
    message.innerText = "生成hash开始" // 生成文件hash开始
    const hash = await getFileHash(chunksList)// 生成文件hash
    message.innerText = hash // 显示hash值
    return hash
}

/**
*

  • 获取全部文件内容hash

  • @param {any} fileList
    */
    async function getFileHash(fileList) {
    const spark = new SparkMD5.ArrayBuffer()
    const result = fileList.map((item, key) => getFileContent(item))
    try {
    const contentList = await Promise.all(result)
    for (let i = 0; i < contentList.length; i++) {
    spark.append(contentList[i])
    }

     return spark.end() // 返回hash总值
    

    } catch (e) {
    console.log(e)
    }
    }

/**
*

  • 获取全部文件内容
  • @param {any} file:Blob
  • @returns
    /
    function getFileContent(file) {
    return new Promise((resolve, reject) => {
    const fileReader = new FileReader()
    //读取文件内容
    fileReader.readAsArrayBuffer(file)
    fileReader.onload = e => {
    resolve(e.target.result)
    }
    fileReader.onerror = e => {
    reject(fileReader.error)
    fileReader.abort()
    }
    })
    }

如果上传的文件过大时,读取文件内容计算hash非常耗时,并且会引起 UI 阻塞,导致页面假死,所以我们使用 web-workerworker 线程计算 hash,这样仍可以在主界面正常做交互。( web-worker 使用方式不清楚的参考MDN介绍)具体做法如下:

代码语言:javascript
复制
/*
  • 生成hash
    */
    const calculateHash = (fileList) => {
    message.innerText = "计算hash...";
    return new Promise((resolve, reject) => {
    window.w = new Worker('../js/setMd5.js')
    // 接收子线程内容
    window.w.onmessage = ev => {
    message.innerText = "";
    resolve(ev.data)
    w.terminate() // 停止子线程
    }
    // 发生错误,终止子线程
    window.w.onerror = err => {
    w.terminate()
    reject(error)
    console.log(error.filename, error.lineno, error.message) // 发生错误的文件名、行号、错误内容
    }
    // 发送信息
    window.w.postMessage(fileList)
    })
    }
  • setMd5.js文件:

    代码语言:javascript
    复制
    // 引入spaprk-md5库
    self.importScripts('./spark-md5.min.js')

    //接受主进程发送过来的数据
    self.onmessage = function (e) {
    const fileChunkList = e.data

    getFileHash(fileChunkList)
        .then(hash =&gt; {
            self.postMessage({
                hash: hash,
            })
        })
        .catch(() =&gt; {
            self.postMessage({
                error: &#39;crate hash error&#39;,
            })
        })
    

    }

    /**
    *

    • 获取全部文件内容hash

    • @param {any} fileList
      */
      async function getFileHash(fileList) {
      const spark = new SparkMD5.ArrayBuffer()
      const result = fileList.map((item, key) => getFileContent(item))

      try {
      const contentList = await Promise.all(result)
      for (let i = 0; i < contentList.length; i++) {
      spark.append(contentList[i])
      }
      return spark.end()
      } catch (e) {
      console.log(e)
      }
      }

    /**
    *

    • 获取全部文件内容

    • @param {any} file

    • @returns
      /
      function getFileContent(file) {
      return new Promise((resolve, reject) => {
      const fileReader = new FileReader()
      //读取文件内容
      fileReader.readAsArrayBuffer(file)
      fileReader.onload = e => {
      resolve(e.target.result)
      }
      fileReader.onerror = e => {
      reject(fileReader.error)
      fileReader.abort()
      }
      })
      }

    流程图

    16659ae3e7f674.png

    hash值+索引号命名切片

    在切片上传uploadChunks方法中调用生成文件hash代码得到hash值,将hash值+索引号作为切片名字上传.

    16659ae67cbe6e.png

    并发控制切片上传

    并发控制具体实现我们在"面试官:为什么网盘上传多个视频文件不能一起上传,80%人回答不清楚!"一文中有详细介绍,可以通过自己封装并发控制函数实现,也可以使用pLimit库实现。

    代码语言:javascript
    复制
    /*

  • 限制多个并发任务,只能同时执行maxCount个

  • maxCount: 最大并发数
    */
    function harexsLimit(maxCount) {
    let activeCount = 0 // 激活任务数
    let waitTask = [] // 任务队列

    const execute = (asyncFn, ...args) => {
    return new Promise((resolve, reject) => {
    const task = create(asyncFn, args, resolve, reject)
    if (activeCount >= maxCount) {
    waitTask.push(task)
    } else {
    task()
    }
    })
    }
    /**

    • 创建待执行任务
      */
      const create = (asyncFn, args, resolve, reject) => {
      return () => {
      asyncFn(...args).then(resolve).catch(reject).finally(() => {
      activeCount--
      // 每执行完一个任务启动任务任务队列下个任务
      if (waitTask.length) {
      waitTask.shift()() //执行任务
      }
      })
      activeCount++
      }
      }

    return execute
    }

  • 16659aeab0c090.png

    总结

    大文件切片生成hash时,如果文件过大,hash值计算会比较慢,还有一种方式就是计算抽样 Hash,减少计算的字节数可以大幅度减少耗时;在前文的代码中,我们是将大文件切片后,全量传入 spark-md5.min.js 中来根据文件的二进制内容计算文件的 hash 的。那么,举个例子,我们可以这样优化: 文件切片以后,取第一个和最后一个切片全部内容,其他切片的取首中尾 三个地方各2各字节来计算 hash。这样来计算文件 hash 会快很多。

    本文共 659 个字数,平均阅读时长 ≈ 2分钟