md5 文件上传

当用户在操作文件上传功能时,某些文件特别大,比如:100M,1G ?G 。网速慢,浏览器卡顿,可使用文件切片方式上传。

html 页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>md5 文件上传</title>
  <style>
    .loading {
      width: 300px;
      border: 1px solid #333;
      height: 20px;
    }
    .loading div {
      width: 0%;
      background: #F00;
      height: 20px;
    }
    
  </style>
</head>
<body>
  <input type="file">
  <div id="loading" class="loading"><div></div></div>
  <!-- md5 文件 网上可下载 -->
  <script src="./md5.js"></script>
  <script>
    const fileEl = document.querySelector('input')
    const loading = document.querySelector('#loading > div')
    const chunkSize = 1024 * 1024* 2;
    fileEl.onchange = async (e) => {
      const ele = e.target;
      const file = ele.files[0];
      // 文件分片
      const chunks = createFileChunk(file, chunkSize)
      
      // 判断文件是否重复
      const hash = await calculateHashSample(file);
      // 1、文件上传
      await calculateHashWorker(chunks)
      // 2、利用浏览器空闲时间上传
      // await calculateHashIdle(chunks)
      const data = chunks.map((chunk, index)=> {
        return {
          hash,
          index,
          chunk: chunk.file,
          progress: 0
        }
      })
      console.log(data)
    }
  </script>
</body>
</html>

文件切片

function createFileChunk(file, size) {
    const chunks = [];
    let cur = 0;
    while (cur < file.size) {
      chunks.push({
        index: cur,
        file: file.slice(cur, cur + size) // 文件切割
      })
      cur += size;
    }
    // 返回切分完成后的文件
    return chunks;
  }

文件上传

FileReader() 对象允许Web应用程序异步读取存储在用户计算机上的文件或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。

async function calculateHashWorker(chunks) {
    return new Promise(reslove => {
      let count = 0;
      const spark = new SparkMD5.ArrayBuffer();
      let progress = 0;
      const loadNext = index => {
        const reader = new FileReader();
        reader.onload = e => {
          count++;
          spark.append(e.target.result)
          if(count === chunks.length) {
            spark.end();
            console.log('完成')
            loading.style.width = (100) + '%';
            reslove();
          } else {
            progress += 100 / chunks.length; // 进度条
            console.log('上传中...',progress)
            loading.style.width = (progress) + '%';
            // 进行下一步上传
            loadNext(count);
          }
        }
        reader.readAsArrayBuffer(chunks[count].file)
      }
      loadNext(0);

    })
  }

利用空闲时间上传

window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

async function calculateHashIdle(chunks) {
    return new Promise(reslove => {
      const spark = new SparkMD5.ArrayBuffer();
      let count = 0;
      const appendToSpark = async file => {
        return new Promise(reslove => {
          const reader = new FileReader();
          reader.readAsArrayBuffer(file)
          reader.onload = e => {
            spark.append(e.target.result)
            reslove();
          }
        })
      }

      const workLoop = async deadline => {
        // 判断当前文件片段是否上传完成 &&  当前剩余时间大于 1
        // deadline.timeRemaining() 获取当前帧的剩余时间
        while (count < chunks.length && deadline.timeRemaining() > 1) {
          await appendToSpark(chunks[count].file)
          count++;
          if(count < chunks.length) {
            // 加载动画
            console.log('加载动画...',((100 * count) / chunks.length).toFixed(2) + '%');
            loading.style.width = ((100 * count) / chunks.length).toFixed(2) + '%';

          } else {
            console.log('加载完毕')
            loading.style.width = '100%';
            reslove(spark.end())
          }
        }
        window.requestIdleCallback(workLoop)
      }
      // 浏览器一旦空闲,调用 workLoop
      window.requestIdleCallback(workLoop)
    })
  }

判断文件是否存在

new Blob() 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作

// 采用抽样逻辑 比如: 1G 的文件,抽样后5M以内
  // 两个文件hash 一样,可能文件不一样,hash 不一样,文件一定不一样。
  async function calculateHashSample(file) {
    // 抽样
    return new Promise(reslove => {
      const spark = new SparkMD5.ArrayBuffer();
      const reader = new FileReader();
      const size = file.size;
      let offset = 1024 * 1024* 2;
      let chunks = [file.slice(0, offset)];//第一个2M,最后一个区块数据全要

      let cur = offset;

      while(cur < size) {
        if(cur + offset >= size) { 
          // 最后一个区块
          chunks.push(file.slice(cur, cur+offset))
        } else {
          // 中间区块
          const mid = cur + offset / 2;
          const end = cur + offset;
          chunks.push(file.slice(cur, cur + 2)) // 起始位置,取两个字节
          chunks.push(file.slice(mid, mid + 2)) // 中间位置,取两个字节
          chunks.push(file.slice(end - 2, end)) // 最后位置,取两个字节
        }
        cur += offset;
      }

      reader.onload = e => {
        spark.append(e.target.result)
        console.log('完成')
        reslove(spark.end());
      }
      reader.readAsArrayBuffer(new Blob(chunks))
    })
  }

注意:坏处:抽样逻辑可能会出现误判概率低,,好处:可以提高效率