乐趣区

关于file:面试官桀桀一笑你没做过大文件上传功能那你回去等通知吧

  • 本文略长,倡议珍藏,文末会附上残缺前后端代码(vue2&vue3+springboot
  • 对付算是一套解决方案吧😁😁😁
  • 前端 vscode 大家都有,后端大家须要下载一个 idea,搞一下 maven,这一点能够请后端共事帮忙
  • 对于一般的单个的大文件上传需要,应该能够应答
  • 笔者本地测试,两三个 G 的大文件没有问题,线上嘛,你懂的

大文件上传问题形容

问题背景

笔者的一个好友上个月被裁,最近在面试求职,在面试时,最初一个问题是问他有没有做过大文件上传性能,我敌人说没做过 …

当初的待业环境不太好,要求都比之前高一些,当然也有可能面试官刷面试 KPI 的,或者这个岗位不急着找人,缓缓面试呗。毕竟也算是本人的工作量,能写进周报外面 …

对于咱们每个人而言:【生于光明,追赶拂晓 ————《 异兽迷城》】

既然面试官会问,那咱们就一起来看看,大文件上传性能如何实现吧 …

中等文件上传解决方案 -nginx 放行

在咱们工作中,上传性能最常见的就是 excel 的上传性能,一般来说,一个 excel 的大小在 10MB 以内吧,如果有好几十 MB 的 excel,就勉强算是中等文件吧,此时,咱们须要设置 nginx 的 client_max_body_size 值,将其放开,只不过一次上传一个几十 MB 的文件,接口会慢一些,不过也勉强可能承受。

前端手握狼牙棒,后端手持流星锤,对产品朗声笑道:要是不能承受,就请忍耐🙂🙂🙂

然而,如果一个文件有几百兆,或者好几个 G 呢?上述形式就不适合了。

既然 一次性上传不行 ,那么咱们就把 大文件拆分 开来,进行分批、分堆、分片 、一点点上传的操作,等上传完了,再将 一片片文件合并 一起,再 复原成原来的样子 即可

最常见的这个需要,就是视频的上传,比方:腾讯视频创作平台、哔哩哔哩后盾等 …

大文件上传解决方案 - 文件分片

一共三步即可:

  • 第一步,大文件拆分成一片又一片(分片操作)
  • 第二步,每一次申请给后端带一片文件(分片上传)
  • 第三步,当每一片文件都上传完,再发申请告知后端将分片的文件合并即可(合并分片)

文件分片操作大抵可分为上述三步骤,但在这三步骤中,还有一些细节须要咱们留神,这个后文中会一一说到,咱们持续往下浏览

大文件上传效果图

为便于更好了解,咱们看一下曾经做好的效果图:

由上述效果图,咱们能够看到,一个 58MB 的大文件,被分成了 12 片上传,很快啊!上传实现。

思考两个问题:

  1. 若某个文件曾经存在(已经上传过),那我还须要上传吗?
  2. 若同一时刻,两个人都在分片上传完大文件,并发动合并申请,如何能力保障不合并错呢?如 A 文件分片成 a1,a2,a3;B 文件分片成 b1,b2,b3。合并操作必定不能把 a1,a2,a3 文件内容合并到 B 文件中去。

解决方案就是:

  • 要告知后端我这次上传的文件是哪一个,下次上传的文件又是哪一个
  • 就像咱们去批改表格中的某条数据时,须要有一个固定的参数 id,告知后端去 update 具体的那一条数据
  • 晓得具体的文件 id,就不会操作错了

那新的问题又来了:

前端如何能力确定文件的 id,如何能力失去文件的惟一标识?

如何失去文件惟一标识?

树上没有两片雷同的叶子,天上没有两朵雷同的云彩,文件是举世无双的(前提是内容不同,复制一份的不算)

who know?

spark-md5 怪笑一声: 寡人通晓!

什么是 spark-md5?

spark-md5 是基于 md5 的一种优良的算法,用途很多,其中就能够去计算文件的惟一身份证标识 -hash 值

  • 只有文件内容不同(蕴含的二进制 01 不同),那么应用 spark-md5 这个 npm 包包,失去的后果 hash 值就不一样
  • 这个举世无双的 hash 值,就可以看做大文件的 id
  • 发申请时,就能够将这个大文件的 hash 值惟一 id 带着传给后端,后端就晓得去操作那个文件了

当然还有别的工具库,如 CryptoJS 也能够计算文件的 hash 值,不过 spark-md5 更支流、更优良

应用 spark-md5 间接计算整个文件的 hash 值(惟一 id 身份证标识)

间接计算一整个文件的 hash 值:

<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>

<input type="file" @change="changeFile">
<script>
    const inputDom = document.querySelector('input') // 获取 input 文件标签的 dom 元素
    inputDom.onchange = (e) => {let file = inputDom.files[0] // 拿到文件
        let spark = new SparkMD5.ArrayBuffer() // 实例化 spark-md5
        let fileReader = new FileReader() // 实例化文件阅读器
        fileReader.onload = (e) => {spark.append(e.target.result) // 增加到 spark 算法中计算
            let hash = spark.end() // 计算实现失去 hash 后果
            console.log('文件的 hash 值为:', hash);
        }
        fileReader.readAsArrayBuffer(file) // 开始浏览这个文件,浏览实现触发 onload 办法
    }
</script>

间接计算一个整文件的 hash 值,文件小的话,还是比拟快的,然而当文件比拟大的时,间接计算一整个文件的 hash 值,就会比较慢了。

此刻大文件分片的益处,再一次体现进去:大文件分片不仅仅能够用于发送申请传递给后端,也能够用于计算大文件的 hash 值,间接计算一个大文件 hash 值工作慢,那就拆分成一些小工作,这样效率也晋升了不少

至此,又延长出一个问题,如何给大文件分片?

当咱们想解决一个 A 问题时,咱们发现须要进一步,解决其中蕴含 a1 问题,当咱们想要解决 a1 问题时,咱们发现须要再进一步解决 a1 的外围 a11 问题。当 a11 问题被解决时,a1 也就解决了,与此同时 A 问题也就迎刃而解了

给文件分片操作

  • 文件分片,别名文件分堆,又名文件分块,也叫作文件拆分
  • 类比,一个大的字符串能够截取 slice(切割)成好几个小的字符串
  • 同理,一个大文件也能够 slice 成好多小文件,对应 api: file.slice
  • 文件 file 是非凡的二进制 blob 文件(所以 file 能够用 blob 的办法)
  • 上代码
const inputDom = document.querySelector('input') // 获取 input 文件标签的 dom 元素
inputDom.onchange = (e) => {let file = inputDom.files[0] // 拿到文件
    function sliceFn(file, chunkSize = 1 * 1024 * 1024) {const result = [];
        // 从第 0 字节开始切割,一次切割 1 * 1024 * 1024 字节
        for (let i = 0; i < file.size; i = i + chunkSize) {result.push(file.slice(i, i + chunkSize));
        }
        return result;
    }
    const chunks = sliceFn(file)
    console.log('文件分片成数组', chunks);
}

文件分片后果效果图(比方我选了一个 5 兆多的文件去分片):

大文件分片后搭配 spark-md5 计算整个文件的 hash 值

有了上述分好片的 chunks 数组(数组中寄存一片又一片小文件),再联合 spark-md5,应用递归的写法,一片一片的再去读取计算,最终算出后果

/**
* chunks:文件分好片的数组、progressCallbackFn 回调函数办法,用于告知外界进度的
* 因为文件阅读器是异步的,所以要套一层 Promise 不便拿到异步的计算结果
**/ 
function calFileMd5Fn(chunks, progressCallbackFn) {return new Promise((resolve, reject) => {
        let currentChunk = 0 // 筹备从第 0 块开始读
        let spark = new SparkMD5.ArrayBuffer() // 实例化 SparkMD5 用于计算文件 hash 值
        let fileReader = new FileReader() // 实例化文件阅读器用于读取 blob 二进制文件
        fileReader.onerror = reject // 兜一下错
        fileReader.onload = (e) => {progressCallbackFn(Math.ceil(currentChunk / chunks.length * 100)) // 抛出一个函数,用于告知进度
            spark.append(e.target.result) // 将二进制文件追加到 spark 中(官网办法)currentChunk = currentChunk + 1 // 这个读完就加 1,读取下一个 blob
            // 若未读取到最初一块,就持续读取;否则读取实现,Promise 带出后果
            if (currentChunk < chunks.length) {fileReader.readAsArrayBuffer(chunks[currentChunk])
            } else {resolve(spark.end()) // resolve 进来告知后果 spark.end 官网 api
            }
        }
        // 文件读取器的 readAsArrayBuffer 办法开始读取文件,从 blob 数组中的第 0 项开始
        fileReader.readAsArrayBuffer(chunks[currentChunk])
    })
}

应用:

inputDom.onchange = (e) => {let file = inputDom.files[0]
    function sliceFn(file, chunkSize = 1 * 1024 * 1024) {const result = [];
        for (let i = 0; i < file.size; i = i + chunkSize) {result.push(file.slice(i, i + chunkSize));
        }
        return result;
    }
    const chunks = sliceFn(file)
    // 分好片的大文件数组,去计算 hash。progressFn 为进度条函数,需额定定义
    const hash = await calFileMd5Fn(chunks,progressFn)
    // "233075d0c65166792195384172387deb" // 32 位的字符串
}

至此,咱们大文件分片上传操作,曾经实现了三分之一了。咱们曾经实现了大文件的分片和计算大文件的 hash 值惟一身份证 id(实际上,计算大文件的 hash 值,还是挺消耗时长的,优化计划就是开一个辅助线程进行异步计算操作,不过这个是优化的点,文末会提到)

接下来,就到了第二步,发申请环节:将曾经分好片的每一片和这个大文件的 hash 值作为参数传递给后端(当然还有别的参数,比方文件名、文件分了多少片,每次上传的是那一片【索引】等 — 看后端定义)

大文件上传解决方案:

  • 第一步,大文件拆分成一片又一片(分片操作)✔️
  • 第二步,每一次申请给后端带一片文件(分片上传)
  • 第三步,当每一片文件都上传完,再发申请告知后端将分片的文件合并即可

分片上传发申请,一片就是一申请

诗曰:

分片上传发申请,一片就是一申请。

申请之前带校验,这样操作才标准。

分片上传申请前的校验申请

校验逻辑思路如下:

  • 大文件分好片当前,在分片文件上传前,先发个申请带着大文件的惟一身份证标识 hash 值,去问问后端有没有上传过这个文件,或者服务端的这个文件是否上传残缺(比方已经上传一半的时候,忽然断网了,或者刷新网页导致上传中断)
  • 后端去看看曾经操作实现的文件夹中的文件,有没有叫做这个 hash 的,依据有没有返回不同的状态码

比方,如下状态码:

  • 等于 0 示意没有上传过,间接上传
  • 等于 1 已经上传过,不须要再上传了(或:障眼法文件秒传递)
  • 等于 2 示意已经上传过一部分,当初要持续上传

对应前端代码:

以下代码举例是 vue3 的语法举例,大家晓得每一步做什么即可,文章看完,倡议大家去笔者的 github 仓库把前后端代码,都拉下来跑起来,联合代码中的正文,才可能更好的了解

html 构造

<template>
  <div id="app">
    <input ref="inputRef" class="inputFile" type="file" @change="changeFile" />
    <div> 大文件 <span class="bigFileC">📁</span> 分了 {{chunksCount}} 片:</div>
    <div class="pieceItem" v-for="index in chunksCount" :key="index">
      <span class="a">{{index - 1}}</span>
      <span class="b">📄</span>
    </div>
    <div> 计算此大文件的 hash 值进度 </div>
    <div class="r"> 后果为: {{fileHash}}</div>
    <progress max="100" :value="hashProgress"></progress> {{hashProgress}}%
    <div>
      <div> 上传文件的进度 </div>
      <div class="r" v-show="fileProgress == 100"> 文件上传实现 </div>
      <progress max="100" :value="fileProgress"></progress> {{fileProgress}}%
    </div>
  </div>
</template>

发校验申请

/**
 * 发申请,校验文件是否上传过,分三种状况:见:fileStatus
 * */
export function checkFileFn(fileMd5) {return new Promise((resolve, reject) => {resolve(axios.post(`http://127.0.0.1:8686/bigfile/check?fileMd5=${fileMd5}`))
    })
}

const res = await checkFileFn(fileMd5);
// res.data.resultCode 为 0 或 1 或 2 

对应后端代码:

笔者后端代码是 springboot,文末会附上代码,大家看一下

private String fileStorePath = "F:\kkk\"; // 大文件上传操作在 F 盘下的 kkk 文件夹中操作

/**
 * @param fileMd5
 * @Title: 判断文件是否上传过,是否存在分片,断点续传
 * @MethodName: checkBigFile
 * @Exception
 * @Description: 文件已存在,1
 * 文件没有上传过,0
 * 文件上传中断过,2 以及当初有的数组分片索引
 */
 
@RequestMapping(value = "/check", method = RequestMethod.POST)
@ResponseBody
public JsonResult checkBigFile(String fileMd5) {JsonResult jr = new JsonResult();
    // 秒传
    File mergeMd5Dir = new File(fileStorePath + "/" + "merge" + "/" + fileMd5);
    if (mergeMd5Dir.exists()) {mergeMd5Dir.mkdirs();
        jr.setResultCode(1);// 文件已存在
        return jr;
    }
    // 读取目录里的所有文件
    File dir = new File(fileStorePath + "/" + fileMd5);
    File[] childs = dir.listFiles();
    if (childs == null) {jr.setResultCode(0);// 文件没有上传过
    } else {jr.setResultCode(2);// 文件上传中断过,除了状态码为 2,还有已上传的文件分片索引
        List<String> list = Arrays.stream(childs).map(f->f.getName()).collect(Collectors.toList());
        jr.setResultData(list.toArray());
    }
    return jr;
}

前端依据接口的状态码,作相应管制,没上传过失常操作,已经上传过了,就做个提醒文件已上传。这里须要特地留神一下,已经上传中断的状况

特地状况:以后上传的文件已经中断过(断点续传)

咱们来捋一下逻辑就清晰了:

  • 假如一个大文件分为了 10 片,对应文件片的索引是 0~9
  • 在执行上传的时候,发了 10 个申请,别离带上对应的索引文件片
  • 因为不可抗力因素,导致只上传胜利了 3 片文件,别离是索引 0、索引 8、索引 9
  • 还有索引 1、2、3、4、5、6、7 这七片文件没上传胜利
  • 那么在查看文件时,后端除了返回状态码 2,同时也返回后端曾经上传胜利的片的索引有哪些
  • 即:{resultCode:2 , resultData:[0,8,9]}
  • 咱们在执行上传文件操作时候,去掉这三个曾经上传实现的即可,上传那些未实现的
// 等于 2 示意已经上传过一部分,当初要持续上传
if (res.data.resultCode == 2) {
    // 若是文件曾上传过一部分,后端会返回上传过得局部的文件索引,前端通过索引能够晓得哪些
    // 上传过,做一个过滤,已上传的文件就不必持续上传了,上传未上传过的文件片
    doneFileList = res.data.resultData.map((item) => {return item * 1; // 后端给到的是字符串索引,这里转成数字索引});
}

doneFileList 数组存储的就是后端返回的,已经上传过一部分的数组分片文件索引

比方上面这两张图,就是文件已经上传中断当前的,再次上传的查看接口返回的数据

示例图一:

返回的是分片文件的名,也就是分片的索引,如下图:

前端依据 doneFileList 判断,去筹备参数

  // 阐明没有上传过,组装一下,间接应用
  if (doneFileList.length == 0) {formDataList = chunks.map((item, index) => {// 后端接参大抵有:文件片、文件分的片数、每次上传是第几片(索引)、文件名、此残缺大文件 hash 值
      // 具体后端定义的参数 prop 属性名,看他们如何定义的,这个不妨...
      let formData = new FormData();
      formData.append("file", item); // 应用 FormData 能够将 blob 文件转成二进制 binary
      formData.append("chunks", chunks.length);
      formData.append("chunk", index);
      formData.append("name", fileName);
      formData.append("md5", fileMd5);
      return {formData};
    });
  }
  // 阐明已经上传过,须要过滤一下,已经上传过的就不必再上传了
  else {
    formDataList = chunks
      .filter((index) => {return !doneFileList.includes(index);
      })
      .map((item, index) => {let formData = new FormData();
        // 这几个是后端须要的参数
        formData.append("file", item); // 应用 FormData 能够将 blob 文件转成二进制 binary
        formData.append("chunks", chunks.length);
        formData.append("chunk", index);
        formData.append("name", fileName);
        formData.append("md5", fileMd5);
        return {formData};
      });
  }
  // 带着分片数组申请参数,和文件名 fileName = file.name
  // 筹备一次并发很多的申请
  fileUpload(formDataList, fileName);

上述代码实现了,失常上传以及 已经中断过的文件持续上传 ,这就是 断点续传

上述代码实现了,失常上传以及 已经中断过的文件持续上传 ,这就是 断点续传

上述代码实现了,失常上传以及 已经中断过的文件持续上传 ,这就是 断点续传

应用 Promise.allSettled(arr)并发上传分好片的文件

  • 应用 Promise.allSettled 发申请好一些,挂了的就挂了,不影响后续不挂的分片上传申请
  • Promise.all 则不行,一个挂了都挂了

前端代码

const fileUpload = (formDataList, fileName) => {const requestListFn = formDataList.map(async ({ formData}, index) => {const res = await sliceFileUploadFn(formData);
    // 每上传完毕一片文件,后端告知已上传了多少片,除以总片数,就是进度
    fileProgress.value = Math.ceil((res.data.resultData / chunksCount.value) * 100
    );
    return res;
  });
  // 应用 allSettled 发申请好一些,挂了的就挂了,不影响后续不挂的申请
  Promise.allSettled(requestListFn).then((many) => {// 都上传完毕了,文件上传进度条就为 100% 了});
};

后端代码

/**
 * 上传文件
 * @param param
 * @param request
 * @return
 * @throws Exception
 */
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public JsonResult filewebUpload(MultipartFileParam param, HttpServletRequest request) {JsonResult jr = new JsonResult();
    boolean isMultipart = ServletFileUpload.isMultipartContent(request);
    // 文件名
    String fileName = param.getName();
    // 文件每次分片的下标
    int chunkIndex = param.getChunk();
    if (isMultipart) {File file = new File(fileStorePath + "/" + param.getMd5());
        if (!file.exists()) { // 没有文件创建文件
            file.mkdir();}
        File chunkFile = new File(fileStorePath + "/" + param.getMd5() + "/" + chunkIndex);
        try {FileUtils.copyInputStreamToFile(param.getFile().getInputStream(), chunkFile); // 流文件操作
        } catch (Exception e) {jr.setResultCode(-1);
            e.printStackTrace();}
    }
    logger.info("文件 -:{}的小标 -:{}, 上传胜利", fileName, chunkIndex);
    File dir = new File(fileStorePath + "/" + param.getMd5());
    File[] childs = dir.listFiles();
    if(childs!=null){jr.setResultData(childs.length); // 返回上传了几个,即为上传进度
    }
    return jr;
}

最初别忘了去合并这些文件分片

添一个上传文件的效果图

  • 由上述动态图,咱们能够看到把文件切割成了 12 份,所以发送了 12 个上传分片申请
  • 当然,上传实现当前,最初,再发一个申请,告知后端去合并这些一片片文件即可
  • 即 merge 申请,当然也要带上此大文件的 hash 值
  • 告知后端具体合并哪一个文件,这样才不会出错

前端代码

// 应用 allSettled 发申请好一些,挂了的就挂了,不影响后续不挂的申请
Promise.allSettled(requestListFn).then(async (many) => {
    // 都上传完毕了,文件上传进度条就为 100% 了
    fileProgress.value = 100;
    // 最初再告知后端合并一下曾经上传的文件碎片了即可
    const loading = ElLoading.service({
      lock: true,
      text: "文件合并中,请稍后📄📄📄...",
      background: "rgba(0, 0, 0, 0.7)",
    });
    const res = await tellBackendMergeFn(fileName, fileHash.value);
    if (res.data.resultCode === 0) {console.log("文件并合胜利, 大文件上传工作实现");
      loading.close();} else {console.log("文件并合失败, 大文件上传工作未实现");
      loading.close();}
});

后端代码

    /**
     * 分片上传胜利之后,合并文件
     * @param request
     * @return
     */
    @RequestMapping(value = "/merge", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult filewebMerge(HttpServletRequest request) {
        FileChannel outChannel = null;
        JsonResult jr = new JsonResult();
        int code =0;
        try {String fileName = request.getParameter("fileName");
            String fileMd5 = request.getParameter("fileMd5");
            // 读取目录里的所有文件
            File dir = new File(fileStorePath + "/" + fileMd5);
            File[] childs = dir.listFiles();
            if (Objects.isNull(childs) || childs.length == 0) {jr.setResultCode(-1);
                return jr;
            }
            // 转成汇合,便于排序
            List<File> fileList = new ArrayList<File>(Arrays.asList(childs));
            Collections.sort(fileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {return -1;}
                    return 1;
                }
            });
            // 合并后的文件
            File outputFile = new File(fileStorePath + "/" + "merge" + "/" + fileMd5 + "/" + fileName);
            // 创立文件
            if (!outputFile.exists()) {File mergeMd5Dir = new File(fileStorePath + "/" + "merge" + "/" + fileMd5);
                if (!mergeMd5Dir.exists()) {mergeMd5Dir.mkdirs();
                }
                logger.info("创立文件");
                outputFile.createNewFile();}
            outChannel = new FileOutputStream(outputFile).getChannel();
            FileChannel inChannel = null;
            try {for (File file : fileList) {inChannel = new FileInputStream(file).getChannel();
                    inChannel.transferTo(0, inChannel.size(), outChannel);
                    inChannel.close();
                    // 删除分片
                    file.delete();}
            } catch (Exception e) {
                code =-1;
                e.printStackTrace();
                // 产生异样,文件合并失败,删除创立的文件
                outputFile.delete();
                dir.delete();// 删除文件夹} finally {if (inChannel != null) {inChannel.close();
                }
            }
            dir.delete(); // 删除分片所在的文件夹} catch (IOException e) {
            code =-1;
            e.printStackTrace();} finally {
            try {if (outChannel != null) {outChannel.close();
                }
            } catch (IOException e) {e.printStackTrace();
            }
        }
        jr.setResultCode(code);
        return jr;
    }
}

至此,大文件上传的三步都实现了

大文件上传解决方案:

  • 第一步,大文件拆分成一片又一片(分片操作)✔️
  • 第二步,每一次申请给后端带一片文件(分片上传)✔️
  • 第三步,当每一片文件都上传完,再发申请告知后端将分片的文件合并即可✔️
  • 笔者用本机测试了一下,两三个 G 的文件都是没有问题的
  • 理论我的项目上线,大文件上传性能,会受到网络带宽、设施性能等各种因素影响
  • 肯定要留神文件分片时切分的大小,例:CHUNK_SIZE = 5 * 1024 * 1024;
  • 这个文件的大小决定了切割多少片,决定了并发多少申请(不可过大,也不可能十分小)
  • 太大单个申请就太慢了,太小浏览器一次发几千上万个申请,也扛不住

辅助线程去优化

开启辅助线程计算大文件的 hash 值

首先,定义函数异步,开启辅助线程,计算

const calFileMd5ByThreadFn = (chunks) => {return new Promise((resolve) => {worker = new Worker("./hash.js"); // 实例化一个 webworker 线程
    worker.postMessage({chunks}); // 主线程向辅助线程传递数据,发分片数组用于计算
    worker.onmessage = (e) => {const { hash} = e.data; // 辅助线程将相干计算数据发给主线程
      hashProgress.value = e.data.hashProgress; // 更改进度条
      if (hash) {
        // 当 hash 值被算进去时,就能够敞开主线程了
        worker.terminate();
        resolve(hash); // 将后果带进来
      }
    };
  });
};

而后,在 public 目录下新建 hash.js 去撰写辅助线程代码

// 应用 importScripts 引入 cdn 应用
self.importScripts('https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js')
self.onmessage = e => {const { chunks} = e.data // 获取到分片数组
    const spark = new self.SparkMD5.ArrayBuffer() // 实例化 spark 对象用于计算文件 hash
    let currentChunk = 0
    let fileReader = new FileReader()
    fileReader.onload = (e) => {spark.append(e.target.result)
        currentChunk = currentChunk + 1
        if (currentChunk < chunks.length) {fileReader.readAsArrayBuffer(chunks[currentChunk])
            // 未曾计算完只告知主线程计算进度
            self.postMessage({hashProgress: Math.ceil(currentChunk / chunks.length * 100)
            })
        } else {
            // 计算完了进度和 hash 后果就都能够告知了
            self.postMessage({hash: spark.end(),
                hashProgress: 100
            })
            self.close();}
    }
    fileReader.readAsArrayBuffer(chunks[currentChunk])
}

应用的话,间接传递分好片文件数组参数即可

const fileMd5 = await calFileMd5ByThreadFn(chunks); // 依据分片计算
console.log('hash',fileMd5) // 得出此大文件的 hash 值了

单纯计算加减乘除啥的倒是能够应用 vue-worker 这个插件,参见笔者之前的文章:https://segmentfault.com/a/1190000043411552

这样的话,速度就会快一些了 …

附录

大文件上传流程图

  • 当咱们把上述文章读完当前,一个大文件上传的流程图就清晰的浮现在咱们的脑海中了
  • 笔者用 processOn 画了一个流程图,如下:

代码仓库

代码仓库:https://github.com/shuirongshuifu/bigfile

欢送 star,您的认可是咱创作的能源哦

当下后端代码是 java 共事 涛哥 提供的,感激之。

后续闲暇了(star 多了),笔者再补充 node 版本的后端代码吧

参考资料

  • webuploader(百度团队开源我的项目):http://fex.baidu.com/webuploader/
  • 大文件上传:https://juejin.cn/post/7177045936298786872
  • Buzut:https://github.com/Buzut/huge-uploader

思考

  • 到这里的话,一般公司的大文件上传需要(一次上传一个),基本上对付解决
  • 本文的内容也应该基本上能应酬面试官了
  • 然而如何能力本人做到相似百度网盘那种上传成果?请钻研 webuploader 源码
  • 道阻且长,还是须要咱们继续优化的 …
退出移动版