计算文件MD5,Web Worker优化计算文件MD5

367 阅读6分钟

如有不妥,多多指教!

  • 需要了解使用的2个包,问题不大~
  • spark-md5
  • p-limit
  • 本文渐进,没有耐心的兄弟们可以直接去看第3点
  1. 整个文件直接计算MD5

import SparkMD5 from 'spark-md5'
const spark = new SparkMD5.ArrayBuffer() 
export const createMD5 = (file: File) => {
  // 验证file
  return new Promise((resolve, reject) => {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onerror = (e) => {
      console.error(e);
      return reject(e);
    };
    fileReader.onload = () => {
      try {
        resolve(spark.hash(dataBuffer));
      } catch (e) {
        console.error(e);
        return reject(e);
      }
    };
  });
};

直接加载整个文件,将整个文件的二进制数据arrayBuffer丢给计算工具计算MD5,如果是一个小文件就还好,但是大文件,批量的文件,一下子将大量资源放到内存里会造成明显的卡顿 .

使用

const uploadFolder = async (file: any) => {
  try {
    let md5 = await createMD5(file);
    console.log(md5);
  } catch (e) {
    // ElMessage.error('上传失败,请重试!');
  }
};
  1. 将文件分片后计算MD5

utils/getMD5.ts

import SparkMD5 from 'spark-md5'

export const BASE_SIZE: number = 1024 * 1024; // 1MB

// 将文件切片,返回切片后的分片list
export const createFileChunk = (file: File): Blob[] => {
  const chunkSize: number = getChunkSize(file);
  const chunks: Blob[] = [];
  let startPos = 0;
  while (startPos < file.size) {
    chunks.push(file.slice(startPos, startPos + chunkSize));
    startPos += chunkSize;
  }
  return chunks;
};

// 获取分片大小
export const getChunkSize = (file: File): number => {
  const fileSize = file.size; // 文件大小
  let chunkSize: number;
  if (fileSize <= 5 * BASE_SIZE) {
    // 0-5M,不分片
    chunkSize = fileSize;
  } else if (fileSize <= 20 * BASE_SIZE) {
    // 5-20M,每个分片大小1M
    chunkSize = BASE_SIZE;
  } else if (fileSize <= 50 * BASE_SIZE) {
    // 20-50M,每个分片大小2M
    chunkSize = 2 * BASE_SIZE;
  } else if (fileSize <= 100 * BASE_SIZE) {
    // 50-100M,每个分片大小4M
    chunkSize = 4 * BASE_SIZE;
  } else if (fileSize <= 200 * BASE_SIZE) {
    // 100-200M,每个分片大小6M
    chunkSize = 6 * BASE_SIZE;
  } else {
    // 每个分片大小8M,不分多了,因为一个 几十M 上传也很慢
    chunkSize = 8 * BASE_SIZE;
  }
  // 根据自己的需求修改计算分片
  // 因为网速慢,我在计算分片设置不大,分片上传的时候,一片一个请求也会快一些,就是请求会变多
  // else if (fileSize <= 500 * BASE_SIZE) {
  //   // 200-500M,每个分片大小10M
  //   chunkSize = 10 * BASE_SIZE;
  // } else if (fileSize <= 1024 * BASE_SIZE) {
  //   // 500M-1G,每个分片大小20M
  //   chunkSize = 20 * BASE_SIZE;
  // } else if (fileSize <= 2 * 1024 * BASE_SIZE) {
  //   // 1G-2G,每个分片大小30M
  //   chunkSize = 30 * BASE_SIZE;
  // } else if (fileSize <= 4 * 1024 * BASE_SIZE) {
  //   // 2G-4G,每个分片大小40M
  //   chunkSize = 40 * BASE_SIZE;
  // } else {
  //   // 4G以上,每个分片大小50M
  //   chunkSize = 50 * BASE_SIZE;
  // }
  return chunkSize;
};
// 计算文件的MD5值
export const getFileMD5 = (chunksList: Blob[]): Promise<any> => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();
    // 当前分片下标
    let currentChunk = 1;
    // 分片总数(向下取整)
    const chunks = chunksList.length;
    // MD5加密开始时间
    const startTime = new Date().getTime();
    loadNext();
    // fileReader.readAsArrayBuffer操作会触发onload事件
    fileReader.onload = function (e) {
      console.log('currentChunk :>> ', currentChunk);
      if (e.target && e.target.result instanceof ArrayBuffer) {
        spark.append(e.target.result);
      } else {
        console.error('Failed to read file as ArrayBuffer: ');
        // 在这里可以添加更多的错误处理逻辑
      }
      if (currentChunk < chunks) {
        console.log(currentChunk, chunks);
        currentChunk++;
        loadNext();
      } else {
        // 该文件的md5值
        const md5 = spark.end();
        console.log(`MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`);
        // 回调传值md5
        resolve(md5);
      }
    };
    fileReader.onerror = function () {
      // ElMessage({
      //   message: '文件读取错误,请重试',
      //   type: 'error',
      // });
      resolve('');
    };
    // 加载下一个分片
    function loadNext() {
      // 文件分片操作,读取下一分片(fileReader.readAsArrayBuffer操作会触发onload事件)
      fileReader.readAsArrayBuffer(chunksList[currentChunk - 1]);
    }
  });
};

使用

import { createFileChunk, getFileMD5 } from '@/utils/getMD5.ts';
const uploadFolder = async (file: any) => {
  const chunksList: Blob[] = createFileChunk(file);
  try {
    let md5 = await getFileMD5(chunksList);
    console.log(md5);
    // 根据md5和chunksList分片,可以做分片上传了
    // ...
    // ...
  } catch (e) {
    // ElMessage.error('上传失败,请重试!');
  }
};

大文件也不会页面卡顿,只是计算大文件 md5 的时间还是会很久,loading的状态会有所保持.

1.1G 大小文件,25s左右(可能电脑不一样速度不一样哈~此处是我的电脑时长)

  1. Web Worker优化计算文件MD5

utils/getMD5.ts

import pLimit from 'p-limit';

// BlockSize 块大小 (默认 50MB)
// ChunkSize 分片大小 (默认 10MB)
const BlockSize = 50 * 1024 * 1024;
const ChunkSize = 10 * 1024 * 1024;
// 设置最大 Worker 数量
export const MAX_WORKERS = 4;
// 获取当前机器的CPU的逻辑核数:(12个-自己电脑),但是我试了一下,4个目前是自己电脑更快的
// export const MAX_WORKERS = navigator.hardwareConcurrency;

// 获取md5,通过webworker形式提高效率,这种 MD5值 不是文件的md5(因为计算的文件分块 md5 ,再做的合并),但是依然可以作为唯一值来给后端,做分片上传的唯一标识
export const getNewFileMD5 = async (file: File, max_worker = MAX_WORKERS) => {
  if (!file) throw new Error('File is required');
  // MD5加密开始时间
  const startTime = new Date().getTime();
  let md5 = '';

  const blocks = createChunks(file); // 创建块和分片
  const limit = pLimit(max_worker);

  // limit接受一个异步任务
  const workerPromises = blocks.map((block) => {
    return limit(
      () =>
        new Promise((workerResolve, workerReject) => {
          const worker = new Worker(new URL('./hashWorker.ts', import.meta.url));
          const chunkArray = createChunksFromBlock(block);

          // 将块中的所有片发送给 Worker
          worker.postMessage({ chunks: chunkArray });

          worker.onmessage = (event) => {
            if (event.data.hash) {
              workerResolve(event.data.hash); // 返回当前块的哈希
              worker.terminate(); // 销毁worker
            } else if (event.data.error) {
              workerReject(event.data.error);
              worker.terminate();
            }
          };

          worker.onerror = (error) => {
            workerReject('Worker error: ' + error.message);
            worker.terminate();
          };
        }),
    );
  });

  // 等待所有块的哈希返回
  const hashes: string[] = (await Promise.all(workerPromises)) as unknown as string[];
  console.log('所有块的哈希', hashes);
  // 合并哈希值
  const finalSpark = new SparkMD5();
  hashes.forEach((hash) => finalSpark.append(hash));
  md5 = finalSpark.end(); // 计算最终的 MD5 值
  // 这种 MD5值 不是文件的md5,但是依然可以作为唯一值来给后端,做分片上传的唯一标识
  console.log(`worker数量:${max_worker},MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`);

  return md5;
};

// 创建块
export const createChunks = (file: File) => {
  const blocks = [];
  let cur = 0;
  // 分块
  while (cur < file.size) {
    const block = file.slice(cur, cur + BlockSize);
    blocks.push(block); // 保存块本身以计算字节数
    cur += BlockSize;
  }
  return blocks;
};

// 从块中创建分片
export const createChunksFromBlock = (block: Blob) => {
  const result = [];
  let cur = 0;
  while (cur < block.size) {
    const chunk = block.slice(cur, cur + ChunkSize);
    result.push(chunk);
    cur += ChunkSize;
  }
  return result;
};

utils/hashWorker.ts

import SparkMD5 from 'spark-md5';

self.onmessage = function (event) {
  const chunks = event.data.chunks;
  const spark = new SparkMD5.ArrayBuffer();
  let loaded = 0;
  const readChunk = (index: number) => {
    if (index >= chunks.length) {
      // 计算完毕
      self.postMessage({ hash: spark.end() });
      return;
    }
    const fileReader = new FileReader();
    fileReader.onload = (e: any) => {
      spark.append(e.target.result);
      loaded++;
      readChunk(index + 1);
    };
    fileReader.onerror = () => {
      self.postMessage({ error: 'File reading error' });
    };
    fileReader.readAsArrayBuffer(chunks[index]);
  };
  readChunk(0);
};

使用

import { createFileChunk, getNewFileMD5 } from '@/utils/getMD5.ts';
const uploadFolder = async (file: any) => {
  const chunksList: Blob[] = createFileChunk(file);
  try {
    let md5 = await getNewFileMD5(file);
    console.log(md5);
    // 根据md5和chunksList分片,可以做分片上传了
    // ...
    // ...
    // 根据 md5 查询哪些分片上传过了,返回list
    // 剔除已上传的,剩下未上传的 residueChunksList
    // residueChunksList 遍历调用分片上传接口
  } catch (e) {
    // ElMessage.error('上传失败,请重试!');
  }
};

获取md5,通过webworker形式提高效率,这种 MD5值 不是文件的md5(因为计算的文件分块 md5 ,再做的合并),但是依然可以作为唯一值来给后端,做分片上传的唯一标识.

1.1G 大小文件,5s左右,显著提升(可能电脑不一样速度不一样哈~此处是我的电脑时长)

分片批量上传

import pLimit from 'p-limit';

// 仅供参考,每个后台设计的接口也不一样
export const batchUpload = async (uploadObj: Record<string, any>, pLimitNum: number = 5) => {
  // 定义并发数量
  const limit = pLimit(pLimitNum);
  // limit接受一个异步任务
  const input = uploadObj.chunksList.map((item: any) => {
    const formData = new FormData();
    formData.append('file', item.file);
    formData.append('fileMd5', uploadObj.fileMd5);
    formData.append('fileTotalChunks', uploadObj.fileTotalChunks);
    formData.append('blockIndex', item.blockIndex);
    formData.append('fileName', uploadObj.fileName);
    return limit(
      () =>
        new Promise(async (resolve) => {
          // 调用上传分片接口
          const res = await Upload(formData);
          // ...
          // ...
          resolve(res);
        }),
    );
  });
  console.log(input);
  // 使用Promise.all来接收异步任务列表
  return await Promise.all(input);
};

如有不妥,多多指教!

如有不妥,多多指教!

如有不妥,多多指教!