前端实现大文件上传切片和文件秒传的功能

最近在做大文件上传的需求,所以就有了这一系列前端文件上传功能的记录。

首先要实现的需求是文件上传切片的功能。

具体功能点

  1. 计算文件MD5值,用于后端检验文件和秒传功能的实现
  2. 对文件进行切片,分块上传

实现

计算文件MD5值

先使用FileReader进行文件读取。

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

代码

  const handleMD5 = (file) => {
    return new Promise((resolve, reject) => {
      const fileReader = new FileReader();
      fileReader.readAsBinaryString(file);
      fileReader.onload = (event) => {
      ...
      };
    });
  };

使用 spark-md5 库来根据文件内容计算出文件的 MD5 值,MD5值是用来传给后端用于秒传功能和分块上传的标识。

安装

npm install --save spark-md5

代码

  const handleMD5 = (file) => {
    return new Promise((resolve, reject) => {
      const fileReader = new FileReader();
      fileReader.readAsBinaryString(file);
      fileReader.onload = (event) => {
        if (event.target) {
          const md5 = SparkMD5.hashBinary(event.target.result);
          return resolve(md5);
        }
        return reject();
      };
    });
  };

这里有个注意的点,用的是SparkMD5.hashBinary这个方法,不是官网最开始 SparkMD5.hash 的方法,SparkMD5.hashBinary是直接对二进制计算MD5值,SparkMD5.hash是对字符串计算MD5值。

文件秒传

文件秒传,就是后端已经存在了上传的文件,所以当再次上传时会直接提示上传成功,其实就是伪上传啦~

实现

在文件上传前,先计算出文件的 hash,先把 hash 发送给后端进行验证,如果后端找到 hash 相同的文件,则直接返回上传成功的信息即可。

代码

// 预上传
const getPreRequest = async (md5: string, file: any, uploadId?: string) => {
    const res = await bucketAPI.postNameFsUpload(detailId, `${pagePros.path}${file.name}`, {
      md5,
      size: file.size,
      uploadId,
    });
    if (res.code === 200) {
      return res.data;
    }
    return undefined;
};

文件切片

设置一个切片的文件大小值size,这里使用兆作为单位。然后进行切片。

代码

const createFileChunk = (file: FileList, uploadId, size?: number) => {
  const SIZE = (size || configSize) * 1024 * 1024;
  const fileChunkList = [] as any[];
  for (let cur = 0, index = 0; cur < file.size; cur += SIZE) {
    fileChunkList.push({ file: file.slice(cur, cur + SIZE), position: cur, index });
    index += 1;
  }
  return fileChunkList;
};

切片文件上传

这里用一组数组来储存整一个完整需要上传的文件请求。

构建发送请求列表,代码

// 构建发送请求列表
const createRequestList = (fileList: any[], name: string, uid: string, uploadId: string) => {
  // 构建发送请求列表
  const requestList = fileList.map((item, index) => {
    return {
      request: bucketAPI.putNameFsUpload,
      props: [
        detailId,
        `${pagePros.path}${name}`,
        formatFormData({
          blockSeq: item.index,
          file: item.file,
          position: item.position,
          uploadId,
        }),
        // 其他设置参数
        {
          // 取消请求设置
          ...
          // 计算上传进度
          ...
        },
      ],
    };
  });
  return requestList;
};

然后发送请求就好了。可以是并发也可以是一个一个发送。因为后续需求还有一个文件同时上传个数的限制和断点续传的功能,为了方便控制,用的是一个一个发送,所以写了一个迭代函数来执行。并发的话可以直接用promise.all

代码

const loopArray = (index: number, list: any[], uid: string) => {
  // 发送请求
  bucketAPI
    .putNameFsUpload(...list[index].props)
    .then(async (res) => {
    if (res.code === 200 && res.data.done) {
        if (index < list.length - 1) {
          // 继续发送请求
          loopArray(index + 1, list, uid);
        }
        if (index === list.length - 1) {
          // 整一个文件分块传送完毕
          ...
        }
    } else {
       // 文件某一块上传失败
       ...
    }
  })
    .catch((err) => {
   		// 文件上传失败
    ...
  });
};

参考资料

使用FileReader进行文件读取

js-spark-md5

字节跳动面试官:请你实现一个大文件上传和断点续传

前端大文件上传方法(深度好文)

其他上传相关

前端实现显示文件上传进度的功能

前端实现文件断点续传功能

前端实现同时上传文件个数的限制

实现大文件上传等相关功能(汇总)