大文件分片上传(前端TS实现)

news2024/12/29 13:04:58

大文件分片上传

内容

一般情况下,前端上传文件就是new FormData,然后把文件 append 进去,然后post发送给后端就完事了,但是文件越大,上传的文件也就越长,如果在上传过程中,突然网络故障,又或者请求超时,等待过久等等情况,就会导致错误而后又得重新传大文件。所以这时候就要使用分片上传了,就算断网了也能继续接着上传(断点上传),如果是之前上传过这个文件了(服务器还存着),就不需要做二次上传了(秒传)。

7.17实现方式

首先获取文件信息后设定分片大小对文件进行分片(slice函数),而后为文件生成一个hash对文件进行标注。在请求时先验证所上传的文件是否已经存在于服务器(若是则直接提示上传成功,即秒传功能),若不存在或部分存在则需要后端返回上传成功的分片标识数组,前端将使用成功分片数组与原文件分片数组进行处理得到未上传成功的分片,而后将未成功的分片以并发方式上传至后端。上传后即完成了整个文件的上传,向后端发送合并分片请求即完成大文件分片上传,断点续传功能。

7.19实现方式(即彻底完成功能)

步骤:

  1. 由于前端计算md5耗时过长,可能会导致页面卡死,因此考虑使用Web Worker来计算md5,即使用worker.js(使用spark-md5)文件来计算:分片数组,分片哈希数组,文件整体哈希。
  2. 在拿到web worker所得到的分片数组,分片哈希数组,文件整体哈希后,即可开始进行大文件上传工作,前端使用文件整体哈希、文件名以及分片数组作为请求参数调用后端/init接口初始化上传操作。【initSend函数】。
  3. 初始化操作完成后,前端使用文件整体哈希作为参数调用后端/status接口获取此文件分片的状态信息,前端根据后端所返回的状态信息使用filter以及every方法得到:文件是否已经上传的状态existFile、后端存在的文件分片existChunks。若existFile为true,则直接提示上传成功,即秒传功能。【verifyInfo函数】
  4. 若existFile为false,则使用后端返回的existChunks数组与分片数组进行对比,得到未上传成功的分片数组,并将其分片信息(分片哈希值,分片内容,分片序号以及文件整体哈希值)作为formData参数发送至后端/chunk接口(在发送分片时使用并发操作,并限制最大并发数为6)【uploadChunks函数】
  5. 当所有分片发送完成后,前端给后端以文件整体哈希做为参数调用后端/merge接口提示后端可以进行合并操作,后端返回成功消息即完成大文件分片上传,断点续传功能。【mergeFile函数】

演示图

  1. 选择文件:
    在这里插入图片描述

  2. 秒传:
    在这里插入图片描述

  3. 分片上传:
    在这里插入图片描述


代码内容

在后续操作中,完成了对请求的封装以及分片上传的hook的编写,主处理逻辑部分仅仅为下方所示:

const submitUpload = () => {
    const file = FileInfo.value;
    if(!file) {
        return;
    }
    fileName.value = file.name;
    const { mainDeal } = useUpload(file, fileName.value)
    mainDeal()
}

api的封装为下方所示:

import request from "@/utils/request";

export async function initSend(uploadId, fileName, totalChunk) {
  return request({
    url: "/init",
    method: "POST",
    data: {
      uploadId,
      fileName,
      totalChunk,
    },
  });
}

export async function verifyInfo(uploadId) {
  return request({
    url: "/status",
    method: "POST",
    data: {
      uploadId,
    },
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
  });
}
export async function uploadSingle(formData) {
  return request({
    url: "/chunk",
    method: "POST",
    data: {
      formData,
    },
    headers: {
      "Content-Type": "multipart/form-data",
    },
  });
}
export async function mergeFile(uploadId) {
  return request({
    url: "/merge",
    method: "POST",
    data: {
      uploadId,
    },
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  });
}

useUpload钩子函数为:

import { ref } from "vue";
import axios from "axios";
import type { AxiosResponse } from "axios";
import {
  initSend,
  verifyInfo,
  mergeFile,
  uploadSingle,
} from "@/apis/uploadApi";
export function useUpload(fileInfo, filename) {
  const FileInfo = ref<File>(fileInfo);
  const fileName = ref(filename); // 文件名称
  const fileHash = ref(""); // 文件hash
  const fileHashArr = ref([]);
  const chunks = ref([]);

  interface Verify {
    id?: Number;
    uploadId?: String;
    chunkIndex?: Number;
    status?: String;
  }

  const mainDeal = async () => {
    const worker = new Worker(new URL("@/utils/worker.js", import.meta.url), {
      type: "module",
    });
    const file = FileInfo.value;
    console.log(worker);
    console.log("file_info", file);
    worker.postMessage({ file: file });
    worker.onmessage = async (e) => {
      const { data } = e;
      chunks.value = data.fileChunkList;
      fileHashArr.value = data.fileChunkHashList;
      fileHash.value = data.fileMd5;
      console.log("uploadid", fileHash.value);
      const res_init = await initSend(
        fileHash.value,
        fileName.value,
        chunks.value.length
      );
      console.log("res_init", res_init);

      const { existFile, existChunks } = await verify(fileHash.value);
      if (existFile) return;

      uploadChunks(chunks.value, existChunks, fileHashArr.value);
      worker.terminate();
    };
  };

  // 控制请求并发
  const concurRequest = (
    taskPool: Array<() => Promise<Response>>,
    max: number
  ): Promise<Array<Response | unknown>> => {
    return new Promise((resolve) => {
      if (taskPool.length === 0) {
        resolve([]);
        return;
      }
      console.log("taskPool", taskPool);
      const results: Array<Response | unknown> = [];
      let index = 0;
      let count = 0;
      console.log("results_before", results);

      const request = async () => {
        if (index === taskPool.length) return;
        const i = index;
        const task = taskPool[index];
        index++;
        try {
          results[i] = await task();
          console.log("results_try", results);
        } catch (err) {
          results[i] = err;
        } finally {
          count++;
          if (count === taskPool.length) {
            resolve(results);
          }
          request();
        }
      };

      const times = Math.min(max, taskPool.length);
      for (let i = 0; i < times; i++) {
        request();
      }
    });
  };

  // 合并分片请求
  const mergeRequest = async () => {
    return mergeFile(fileHash.value);
  };

  // 上传文件分片
  const uploadChunks = async (
    chunks: Array<Blob>,
    existChunks: Array<string>,
    md5Arr: Array<string>
  ) => {
    const formDatas = chunks
      .map((chunk, index) => ({
        fileHash: fileHash.value,
        chunkHash: fileHash.value + "-" + index,
        chunkIndex: index,
        checksum: md5Arr[index],
        chunk,
      }))
      .filter((item) => !existChunks.includes(item.chunkHash));
    console.log("formDatas", formDatas);
    const form_Datas = formDatas.map((item) => {
      console.log("!", item.chunkIndex);
      const formData = new FormData();
      formData.append("uploadId", item.fileHash);
      formData.append("chunkIndex", String(item.chunkIndex));
      formData.append("checksum", item.checksum);
      formData.append("file", item.chunk);
      return formData;
    });
    console.log("formDatas", form_Datas);
    const taskPool = form_Datas.map(
      (formData) => () =>
        fetch("http://10.184.131.57:8101/ferret/upload/chunk", {
          method: "POST",
          body: formData,
        })
    );
    //控制请求并发
    const response = await concurRequest(taskPool, 6);
    console.log("response", response);
    // 合并分片请求
    const res_merge = await mergeRequest();
    console.log("res_merge", res_merge);
  };

  // 校验文件、文件分片是否存在
  const verify = async (uploadId: string) => {
    const res = await verifyInfo(uploadId);
    const { data } = res.data;
    console.log("verify", res);
    // 看服务器是不是已经有文件所有信息
    const existFile = data.every((item: Verify) => item.status === "Uploaded");
    const existChunks: string[] = [];
    data.filter((item: Verify) => {
      if (item.status === "Uploaded") {
        existChunks.push(`${item.uploadId}-${item.chunkIndex}`);
      }
    });
    console.log("existFile", existFile, "existChunks", existChunks);
    return {
      existFile,
      existChunks,
    };
  };
  return {
    mainDeal,
  };
}

web worker实现方式:

import SparkMD5 from 'spark-md5';
let DefaultChunkSize = 1024 * 1024 * 5; // 5MB

self.onmessage = (e) => {
    console.log("!!>", e.data)

    if (e.data.file.size >= 1024 * 1024 * 100 && e.data.file.size < 1024 * 1024 * 512) {
        DefaultChunkSize = 1024 * 1024 * 10
    }else if (e.data.file.size >= 1024 * 1024 * 512) {
        DefaultChunkSize = 1024 * 1024 * 50
    }

    const { file, chunkSize = DefaultChunkSize } = e.data;
    let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
        chunks = Math.ceil(file.size / chunkSize),
        currentChunk = 0,
        spark = new SparkMD5.ArrayBuffer(),
        fileChunkHashList = [],
        fileChunkList = [],
        fileReader = new FileReader();

    loadNext();

    function loadNext() {
        let start = currentChunk * chunkSize,
            end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;

        let chunk = blobSlice.call(file, start, end);
        fileChunkList.push(chunk);
        fileReader.readAsArrayBuffer(chunk);
    }

    function getChunkHash(e) {
        const chunkSpark = new SparkMD5.ArrayBuffer();
        chunkSpark.append(e.target.result);
        fileChunkHashList.push(chunkSpark.end());
    }

    // 处理每一块的分片
    fileReader.onload = function (e) {
        spark.append(e.target.result);
        currentChunk++;

        getChunkHash(e)

        if (currentChunk < chunks) {
            loadNext();
        } else {
            // 计算完成后,返回结果
            self.postMessage({
                fileMd5: spark.end(),
                fileChunkList,
                fileChunkHashList,
            });
            fileReader.abort();
            fileReader = null;
        }
    }

    // 读取失败
    fileReader.onerror = function () {
        self.postMessage({
            error: 'wrong'
        });
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1947548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

opencascade AIS_InteractiveContext源码学习9 obsolete methods

AIS_InteractiveContext 前言 交互上下文&#xff08;Interactive Context&#xff09;允许您在一个或多个视图器中管理交互对象的图形行为和选择。类方法使这一操作非常透明。需要记住的是&#xff0c;对于已经被交互上下文识别的交互对象&#xff0c;必须使用上下文方法进行…

鸿蒙SDK开发能力

什么是鸿蒙SDK&#xff1a;HarmonyOS(Software Development Kit)是面向应用和服务开发的开放能力合集,本质就是工具集&#xff0c;与JDK、AndroidSDK在逻辑上有相似之处 18N&#xff1a;1指的是手机&#xff0c;8指的是车机、音箱、耳机、手表/手环、平板、大屏、PC、AR/VR&am…

Python——使用Seaborn钻石数据可视化分析(2)

续 Python——使用Seaborn钻石数据可视化分析(1) 目录 📈 4、非数值变量描述性统计分析 1️⃣ 柱状图——分析钻石切工的情况 📍 sns.countplot —— 绘制柱状图、条形图 2️⃣ 箱线图——分析不同切工的钻石的价格情况 📍 sns.barplot —— 不同分类变量之间的数…

用这些宝藏AI工具打造副业!实现被动收入!

前言 大家好&#xff0c;我是月月&#xff01;今天我们来梳理一下在目前的形势下&#xff0c;如何用AI工具打造一个躺赚的副业&#xff0c;实现被动收入&#xff1f;有哪些方法和途径&#xff1f;在本篇文章我主要提供一些已有的AI工具&#xff0c;后面我们再根据具体的AI工具…

国家自然灾害防治研究院专家莅临国信华源公司指导调研

7月16日&#xff0c;应急管理部国家自然灾害防治研究院党委书记杨思全与中关村科技园区丰台园管理委员会副主任王成玉一行莅临北京国信华源公司进行调研指导。我司总经办及相关部门负责人陪同座谈&#xff0c;并详细汇报了企业的运营情况和技术成果。 在我司高层的陪同下&#…

数字孪生:变电站监测和运维的智能化实践

随着夏季高温天气的到来&#xff0c;我国用电也迎来了高峰。用电负荷持续走高&#xff0c;对全国各地电网运维也迎来了挑战。电力系统作为现代社会的基础设施&#xff0c;其稳定性和可靠性至关重要&#xff0c;变电站则是实现电力系统电力互联互通的枢纽。 在传统变电站中&…

VLC输出NDI媒体流

目录 1. 下载安装VLC Play 2. 首先在电脑上安装NDI Tools 3. 运行VLC进行输出配置 4. 播放视频 5. 验证 (1)用Studio Monitor验证 (2)用OBS验证 NDI(Network Device Interface)即网络设备接口,是由美国 NewTek 公司开发的免费标准,它可使兼容的视频产品以高质量…

新书速览|PyTorch深度学习与计算机视觉实践

《PyTorch深度学习与计算机视觉实践》 本书内容 在人工智能的浩瀚星空中&#xff0c;深度学习犹如一颗耀眼的明星&#xff0c;引领着计算机视觉技术的发展。《PyTorch深度学习与计算机视觉实践》带领读者领略深度学习在计算视觉领域的魅力&#xff0c;详解使用PyTorch 2.0进行…

Spring Bean配置文件创建对象

类型&#xff1a; 1、值类型 2、null &#xff08;标签&#xff09; 3、特殊符号 &#xff08;< -> < &#xff09; 4、CDATA <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/bea…

【Python】成功解决conda创建虚拟环境时出现的CondaHTTPError: HTTP 000 CONNECTION FAILED错误

【Python】成功解决conda创建虚拟环境时出现的CondaHTTPError: HTTP 000 CONNECTION FAILED错误 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&a…

乐鑫 Matter 技术体验日回顾|全面 Matter 解决方案驱动智能家居新未来

日前&#xff0c;乐鑫信息科技 (688018.SH) 在深圳成功举办了 Matter 方案技术体验日活动&#xff0c;吸引了众多照明电工、窗帘电机、智能门锁、温控等智能家居领域的客户与合作伙伴。活动现场&#xff0c;乐鑫产研团队的小伙伴们与来宾围绕 Matter 产品研发、测试认证、生产工…

elmentui this.$confirm使用模板字符串构建HTML结构

tip(){const checkingList [];const findList[入会1,入会2,入会3] //数组const sueccList [{name:入会,suecc:1000,numcot:1000},{name:aaaaa,suecc:222,numcot:3333}] //数组对象var message// 使用模板字符串构建HTML结构if(sueccList.length>0){message <div>…

【系统架构设计】数据库系统(二)

数据库系统&#xff08;二&#xff09; 数据库模式与范式数据库设计数据库设计的方法数据库设计的基本步骤 事务管理并发控制故障和恢复 备份与恢复分布式数据库系统数据仓库数据挖掘NoSQL大数据 数据库模式与范式 数据库设计 数据库设计的方法 目前已有的数据库设计方法可分…

Matlab 命令行窗口默认输出(异常)

目录 前言Matlab 先验知识1 异常输出的代码2 正常输出的代码 前言 在单独调试 Matlab 写的函数时出现不想出现的异常打印值&#xff0c;逐个注释排查才找到是 if elseif else 代码块的问题&#xff0c;会默认打印输出 else 部分第一个返回值的值&#xff08;下方代码中的 P值&…

【linux】Shell脚本三剑客之grep和egrep命令的详细用法攻略

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

【MySQL进阶之路 | 高级篇】简谈redo日志

1. 前言 事务有四种特性&#xff1a;原子性&#xff0c;一致性&#xff0c;隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢&#xff1f; 事务的隔离性由锁机制实现。而事务的原子性&#xff0c;一致性和持久性由事务的redo日志和undo日志来保证。 REDO LOG称为…

Mysql或MariaDB数据库的用户与授权操作——实操保姆级教程

一、问题描述 在日常的工作中,我们需要给不同角色的人员创建不同的账号,他们各自可访问的数据库或权限不一样,这时就需要创建用户和赋予不同的权限内容了。 二、问题分析 1、创建不同的角色账号; 2、给这些账号授予各自可访问数据库的权限。 三、实现方法 Centos8安装…

多层感知机(神经网络)

目录 一、感知机&#xff08;逻辑回归、二分类&#xff09;定义&#xff1a;二、感知机不能解决XOR问题&#xff1a;三、多层感知机定义&#xff1a;四、训练过程&#xff1a;1.参数维度&#xff1a;2.常用激活函数&#xff1a;2.1Sigmoid激活函数&#xff1a;2.2Tanh激活函数&…

排序XXXXXXXXX

信息学奥赛&#xff5c;常见排序算法总结&#xff08;C&#xff0b;&#xff09; - 腾讯云开发者社区-腾讯云 (tencent.com) https://cloud.tencent.com/developer/news/975232 常用序号层级排序 一、序号 序号Sequence Number&#xff0c;有顺序的号码&#xff0c;如数字序号…

数据结构: 链表回文结构/分割链表题解

目录 1.链表的回文结构 分析 代码 2.链表分割 ​编辑分析 代码 1.链表的回文结构 分析 这道题的难点是空间复杂度为O&#xff08;1&#xff09; 结合逆置链表找到链表的中间节点就可以解决了。 先找到链表的中间节点&#xff0c;再对中间节点的下一个节点进行逆置&…