前景

说到大文件上传,那必不可少的就是上传文件时计算文件的hash值了,那么我们为什么要计算文件的hash值呢?其实文件的hash值就相当于文件的DNA或者身份证一样,通常我们在上传文件时都会去校验一下服务器是否存在和我们相同的文件时,我们就会用到文件的hash值来比较是否上传重复的文件,像要实现文件妙传、切片等功能计算文件hash也是必不可少的,下面我就列举我们在前端文件上传计算hash的常用的两个方法:window.requestIdleCallback(),抽样hash

requestIdleCallback()

{% note info green %} React 中的Fiber 就是基于这个api去实现的,我们都知道javaScript是单线程的,如果有一些比较大型的计算任务等放在主线程去执行的话,就会造成主线程阻塞,然后导致浏览器渲染的时候卡顿(浏览器渲染网页时都是一帧一帧绘制出来的,如果在一帧中浏览器的任务很快不到16ms(一帧大概16.5ms)就完成任务的话,那么这一帧就会有空闲的时间),而requestIdleCallback()这个函数将在浏览器空闲时期被调用。这使我们能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。 {% endnote %}

文件切片

{% note info green %} 上传文件时,首先对文件进行切片 {% endnote %}

// 文件切片大小,默认是1兆
const CHUNK_SIZE = 1*1024*1024
// 切片
export const createFileChunk = async (file,size=CHUNK_SIZE) => {
    const chunks = [];
    let cur = 0;
    while(cur < file?.size){
        chunks.push({index:cur,file:file.slice(cur,cur+size)})
        cur+=size
    }
    return chunks;
}

计算文件hash值(方式一)

{% note info green %} 这里会用到spark-md5这个库,主要做文件的增量计算,然后我们将得到的切片作为参数,spark.end()最后会返回一串字符串,这就是我们计算出来的hash {% endnote %}

    // chunks 文件切片参数
    // 利用浏览器自带的一个requestIdleCallback函数执行我们的hash计算,这样不会影响浏览器的主线程
    const calculateHashRequestIdle = async (chunks) => {
        return new Promise(resolve => {
            const spark = new SparkMD5.ArrayBuffer();
            let count = 0;
            // 追加
            const appendToSpark = async file => {
                return new Promise(resolve => {
                    const reader = new FileReader();
                    reader.readAsArrayBuffer(file);
                    reader.onload = e => {
                        spark.append(e.target.result);
                        resolve()
                    };
                })
            };
            const workLoop = async deadline => {
                // 获取当前帧的剩余时间
                while (count < chunks.length && deadline.timeRemaining() > 1) {
                    //空闲时间,且有任务
                    await appendToSpark(chunks[count].file);
                    count++;
                    if (count < chunks.length) {
                        // 这里是计算上传进度条的
                        const hashProgress = Number(((100 * count) / chunks.length).toFixed(2));
                        setProgressNum(hashProgress)
                    } else {
                        setProgressNum(100);
                        resolve(spark.end())
                    }
                }
                window.requestIdleCallback(workLoop)
            }
            // 浏览器一有空闲就会调用workloop
            window.requestIdleCallback(workLoop)
        })
    }

文件抽样hash

计算文件hash值

{% note info green %}

  1. 将文件切片为2M
  2. 首先切第一区块的和最后一个区块的全部内容,其他切片去首中尾地方各2各字节
  3. 合并内容,计算md5
  4. 抽样hash的接口,如果文件存在,会出现小概率的误判,通常我们可以用它配合上面的hash做双重验证 {% endnote %} hash
    // 抽样hash计算
    const claculateHashSample = (fileObj) => {
        // hash一样,文件不一定一样
        // hash不一样,文件一定不一样
        return new Promise(resolve => {
            const spark = new SparkMD5.ArrayBuffer();
            const reader = new FileReader();
            const size = fileObj?.size;
            const offset = 2 * 1024 * 1024;
            // 第一个2M,最后一个区块数据全要
            let chunks = [fileObj.slice(0, offset)];
            let cur = offset;
            while (cur < size) {
                // 取最后一个区块
                if (cur + offset >= size) {
                    chunks.push(fileObj.slice(cur, cur + offset))
                } else {
                    // 中间的区块
                    const mid = cur + offset / 2;
                    const end = cur + offset;
                    chunks.push(fileObj.slice(cur, cur + 2));
                    chunks.push(fileObj.slice(mid, mid + 2));
                    chunks.push(fileObj.slice(end - 2, end))
                }
                cur+=offset;
            };
            // 中间的,取前后各2个字节
            reader.readAsArrayBuffer(new Blob(chunks));
            reader.onload = e => {
                spark.append(e.target.result);
                setProgressNum(100)
                resolve(spark.end());
            }
        })
    }

web_woker

{% note info green %} 它的作用就是给JS创造多线程运行环境,允许主线程创建worker线程,分配任务给后者,主线程运行的同时worker线程也在运行,相互不干扰,在worker线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给worker线程执行,这样主线程就会变得轻松,不会被阻塞或拖慢。这并不意味着JS语言本身支持了多线程能力,而是浏览器作为宿主环境提供了JS一个多线程运行的环境。 不建议使用,太麻烦也有一些限制(同源限制、文件限制等。。。),不能作为模块的方式去引入,只能是通过外部文件引入,具体,大家百度查一下就知道了,这里不多赘述 {% endnote %}

// 利用浏览器的web_woker来计算文件的hash值
export const calculateHashWorker = async (chunks) => {
    return new Promise(resolve => {
        const worker = new Worker('./hash.js');
        worker.postMessage({chunks:chunks}) ;
        worker.onmessage = e => {
            const {progress,hash} = e.data
            const  hashProgress = Number(progress.toFixed(2))
            if(hash){
              resolve({ hash,hashProgress})
            }
        }
    })
}

hash.js

// 引入spark-md5
self.importScripts('spark-md5.min.js')
self.onmessage = e=>{
  // 接受主线程传递的数据
  const {chunks } = e.data
  const spark = new self.SparkMD5.ArrayBuffer()
  let progress = 0
  let count = 0
  const loadNext = index=>{
    const reader = new FileReader()
    reader.readAsArrayBuffer(chunks[index].file)
    reader.onload = e=>{
      count ++
      spark.append(e.target.result)

      if(count==chunks.length){
        self.postMessage({
          progress:100,
          hash:spark.end()
        })
      }else{
        progress += 100/chunks.length
        self.postMessage({
          progress
        })
        loadNext(count)
      }
    }
  }
  loadNext(0)
}