【文件上传、秒传、分片上传、断点续传、重传】

news2025/2/8 17:03:09

文章目录

  • 获取文件对象
  • 文件上传(秒传、分片上传、断点续传、重传)
  • 优化

获取文件对象

input标签的onchange方法接收到的参数就是用户上传的所有文件

<html lang="en">
    <head>
        <title>文件上传</title>
        <style>
            #inputFile,#inputDirectory {
                display: none;
            }
            #dragarea{
                width: 100%;
                height: 100px;
                border: 2px dashed #ccc;
            }
            .dragenter{
                background-color: #ccc;
            }
        </style>
    </head>
    <body>
        <!-- 
            1. 如何上传多文件:multiple
            2. 如何上传文件夹:为了兼顾各浏览器兼容性,需设置三个属性:webkitdirectory mozdirectory odirectory
            3. 如何实现拖拽上传:input默认是有拖拽性质的,但是由于浏览器兼容性问题,开发一般不使用,一般使用div阻止默认事件以及通过拖拽api实现
            4. 如何获取选择的所有文件
         -->
         <div id="dragarea"></div>
         <input id="inputFile" type="file" multiple>
         <!-- 如果不想用input自带的上传文件的样式,可以通过button的click触发input的点击事件来上传文件 -->
         <button id="buttonFile">上传文件</button>
         <input id="inputDirectory" type="file" multiple webkitdirectory mozdirectory odirectory>
         <button id="buttonDirectory">上传文件夹</button>
         <ul class="fileList"></ul>
         <script>
            const inputFile = document.getElementById("inputFile")
            const buttonFile = document.getElementById("buttonFile")
            const inputDirectory = document.getElementById("inputDirectory")
            const buttonDirectory = document.getElementById("buttonDirectory")
            const dragarea = document.getElementById("dragarea")
            const fileList = document.getElementById("fileList")
            const appendFile = (fileList) => {
                for(const file in fileList){
                    const li = document.getElementById("li")
                    li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
                    fileList.appendChild(li)
                }
            }
            const traverseFile = (entry) => {
                if(entry.isFile){
                    entry.file((file) => {
                        const li = document.getElementById("li")
                        li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
                        fileList.appendChild(li)
                    })
                }else if(entry.isDirectory){
                    traverseDirectory(entry)
                }
            }
            const traverseDirectory = (directory) => {
                const reader = directory.createReader()// 创建读取器读取文件夹
                reader.readEntries((entries) => {
                    for(const entry of entries) {
                        traverseFile(entry)
                    }
                })
            }
            buttonFile.onclick = () => {
                inputFile.click()
            }
            inputFile.onchange = (e) => {
                const files = e.target.files// 获得用户上传的所有文件
                appendFile(files)
            }
            inputDirectory.onchange = (e) => {
                console.log(e.target.files)
                const files = e.target.files// 获得用户上传的所有文件
                appendFile(files)
            }
            buttonDirectory.onclick = () => {
                inputDirectory.click()
            }
            dragarea.ondragenter = (e) => {
                e.preventDefault();
                console.log("拖拽进入区域")
                dragarea.classList.add("dragenter")
            }
            dragarea.ondragover = (e) => {
                e.preventDefault();
                console.log("拖拽着悬浮在区域上方")
                dragarea.classList.add("dragenter")
            }
            dragarea.ondragleave = (e) => {
                e.preventDefault();
                console.log("拖拽离开")
                dragarea.classList.remove("dragenter")
            }
            // 拖拽放开
            dragarea.ondrop = (e) => {
                e.preventDefault();
                dragarea.classList.remove("dragenter")
                const items = e.dataTransfer.items// 拖拽进来的所有文件
                for(const item of items){
                    const entry = item.webkitGetAsEntry()
                    traverseFile(entry)
                }
            }
         </script>
    </body>
</html>

文件上传(秒传、分片上传、断点续传、重传)

秒传:调用后端的接口,将md5值传过去,后端判断如果这个md5值对应的文件是否已经合并,如果已经合并,则返回文件上传成功
分片上传:每片大小chunk_size为1m,假如文件1.5m,那么会被分成2片,使用file.slice截取[0,1),再截取[1,1.5)
断点续传:文件上传前会调用后端的接口,将md5值传过去,后端判断如果这个md5值对应的文件是否已经合并,如果没有合并,会返回这个md5值已经上传的切片的索引,前端重新上传剩余索引的片
并发控制:假如我们把文件切成了100片,如果一下子把这100片全传给后端,会给后端造成并发压力,所以在发送前可以在前端进行并发控制一下,我们将所有的请求都放在队列里,每次从队列里弹出几个请求来发送

明明浏览器可以控制请求并发,为什么前端还要自己控制并发请求?

  1. 避免浏览器并发限制:浏览器对同一域名的并发请求数量是有限制的(通常是 6-8 个,具体取决于浏览器和协议)。如果前端不控制并发请求,可能会导致大量请求堆积,超出浏览器的并发限制,从而阻塞其他重要请求(如关键 API 或资源加载),
  2. 提升用户体验:如果一次性发送过多请求,可能会导致网络带宽被占满,影响页面其他资源的加载(如图片、CSS、JS 等),并且可能会导致部分请求超时或失败,从而浪费网络资源和用户流量。
  3. 错误处理和重试机制:手动控制并发可以更好地实现错误处理和重试机制。
    例如,某个请求失败后,可以立即重试,而不是等待所有请求完成后再处理错误。
  4. 优先级控制:手动控制并发可以实现请求的优先级管理。例如,某些关键请求可以优先发送,而低优先级的请求可以稍后处理。
  5. 兼容性和稳定性:不同浏览器对并发请求的处理方式可能不同,手动控制并发可以确保应用在各种浏览器中表现一致。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>文件上传</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.7.2/axios.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.js"></script>
    <style>
        #inputFile,
        #inputDirectory {
            display: none;
        }

        #dragarea {
            width: 100%;
            height: 100px;
            border: 2px dashed #ccc;
        }

        .dragenter {
            background-color: #ccc;
        }
    </style>
</head>

<body>
    <div class="dragarea"></div>
    <input class="inputFile" type="file" multiple>
    <!-- 如果不想用input自带的上传文件的样式,可以通过button的click触发input的点击事件来上传文件 -->
    <button class="buttonFile">上传文件</button>
    <input class="inputDirectory" type="file" multiple webkitdirectory mozdirectory odirectory>
    <button class="buttonDirectory">上传文件夹</button>
    <button class="buttonUpload">点击上传</button>
    <ul class="fileListElement"></ul>
    <script>
        // 文件交互相关
        const fileList = []
        const chunk_size = 1 * 1024 * 1024
        const requestQueue = []
        const maxRequest = 2// 最大请求数量
        let currentRequest = 0// 当前请求数
        const inputFile = document.getElementsByClassName("inputFile")[0]
        const buttonFile = document.getElementsByClassName("buttonFile")[0]
        const inputDirectory = document.getElementsByClassName("inputDirectory")[0]
        const buttonDirectory = document.getElementsByClassName("buttonDirectory")[0]
        const dragarea = document.getElementsByClassName("dragarea")[0]
        const fileListElement = document.getElementsByClassName("fileListElement")[0]
        const buttonUpload = document.getElementsByClassName("buttonUpload")[0]
        // 将上传的文件展示在按钮下方
        const showFileList = (files) => {
            for (const file in files) {
                const li = document.getElementById("li")
                li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
                fileListElement.appendChild(li)
                fileList.push(file)
            }
        }
        const traverseFile = (entry) => {
            // 拖拽进来的如果是文件,直接展示在按钮下方
            if (entry.isFile) {
                entry.file((file) => {
                    const li = document.getElementById("li")
                    li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
                    fileList.appendChild(li)
                })
            } else if (entry.isDirectory) {
                // 拖拽进来的如果是文件夹,读文件夹,获得文件夹里面的文件
                traverseDirectory(entry)
            }
        }
        const traverseDirectory = (directory) => {
            const reader = directory.createReader()
            reader.readEntries((entries) => {
                for (const entry of entries) {
                    traverseFile(entry)
                }
            })
        }
        buttonFile.onclick = () => {
            inputFile.click()
        }
        inputFile.onchange = (e) => {
            const files = e.target.files // 获得用户上传的所有文件
            showFileList(files)
        }
        inputDirectory.onchange = (e) => {
            console.log(e.target.files)
            const files = e.target.files // 获得用户上传的所有文件
            showFileList(files)
        }
        buttonDirectory.onclick = () => {
            inputDirectory.click()
        }
        dragarea.ondragenter = (e) => {
            e.preventDefault();
            console.log("拖拽进入区域")
            dragarea.classList.add("dragenter")
        }
        dragarea.ondragover = (e) => {
            e.preventDefault();
            console.log("拖拽着悬浮在区域上方")
            dragarea.classList.add("dragenter")
        }
        dragarea.ondragleave = (e) => {
            e.preventDefault();
            console.log("拖拽离开")
            dragarea.classList.remove("dragenter")
        }
        // 拖拽放开
        dragarea.ondrop = (e) => {
            e.preventDefault();
            dragarea.classList.remove("dragenter")
            const items = e.dataTransfer.items
            for (const item of items) {
                const entry = item.webkitGetAsEntry()
                traverseFile(entry)
            }
        }
        // 文件上传
        buttonUpload.onclick = () => {
            for (const file of fileList) {
                if (file.size <= chunk_size) {
                    uploadSingleFile(file)
                } else {
                    uploadLargeFile(file)
                }
            }
        }
        // 单文件一整个文件上传
        // 文件上传通过formData传输,因为formData是前后端都认识的格式,file是只有前端才认识的格式(后端不认识)
        const uploadSingleFile = (file) => {
            const formData = new FormData()
            formData.append("file", file) // 通过append往formData身上添加对象,如果formData身上已有file对象,会覆盖
            try {
                axios.post("http://127.0.0.1:3001/upload", formData, {
                    headers: {
                        "content-type": "multipart/form-data"
                    }
                })
            } catch (error) {
                throw error
            }
        }
        // 大文件上传
        const uploadLargeFile = async (file) => {
            // 创建文件hash。创建整个文件的hash即可,每个片不用创建hash,因为每片是调用后端的方法上传的,返回成功即上传成功
            const md5 = await createFileMd5(file)
            // 大文件分片
            const chunksList = createChunkFile(file)
            // 创建文件分片对象
            const chunkListObj = createChunkFileObj(chunksList, file, md5)
            // 将md5值传给后端接口,判断文件是否在服务器上存在,如果存在,后端返回isExistObj.isExists为true,则秒传成功,
            // 如果不存在,后端会返回给你此md5值上传了哪些片,已上传的片的索引放在chunkIds中
            const isExistObj = await juedgeFileExist(file, md5)
            if (isExistObj && isExistObj.isExists) {
                alert('文件已秒传成功!')
                return
            }
            // 文件上传
            await asyncPool(chunkListObj, isExistObj.chunkIds) // chunkIds:后端返回的,已上传的分片的索引
            // await Promise.all(promises)
            concatChunkFile(file, md5)// 文件上传完毕,调用后端合并文件的接口
        }
        // 创建文件的md5值
        const createFileMd5 = (file) => {
            return new Promise((resolve, reject) => {
                const reader = new FileReader()
                // reader.readAsArrayBuffer(file)读取完毕后会调用onload,读取失败调用onerror,读取到的内容在e.target.result中
                reader.onload = (e) => {
                    const md5 = SparkMD5.ArrayBuffer.hash(e.target.result)
                    resolve(md5)
                }
                reader.onerror = () => {
                    reject(error)
                }
                reader.readAsArrayBuffer(file)
            })
        }
        // 创建文件分片:每片大小chunk_size为1m,假如文件1.5m,那么会被分成2片,使用file.slice截取[0,1),再截取[1,1.5)
        const createChunkFile = (file) => {
            let current = 0
            const chunkList = []
            while (current < file.size) {
                chunkList.push(file.slice(current, Math.min(current + chunk_size, file.size)))
                current += chunk_size
            }
            return chunkList
        }
        // 创建文件分片对象。将文件的md5、文件名、本片在整个文件中的索引,都传入这个对象,调用后端接口上传时会用到的数据都可以封装进来
        const createChunkFileObj = (chunkList, file, md5) => {
            return chunkList.map((chunk, index) => {
                return {
                    file: chunk,
                    md5,
                    name: file.name,
                    index: index,
                }
            })
        }
        // 文件分片上传
        const uploadChunkFile = (chunkListObj, chunkIds) => {
            return chunkListObj.filter((item,index) => (!chunkIds.includes(index)))// 过滤掉已经上传的切片,让已经上传的切片没有下面那个函数
            .map((chunk, index) => {
                return () => {
                    const formData = new FormData()
                    formData.append("file", chunk.file, `${chunk.md5}-${chunk.index}`)
                    formData.append("name", chunk.name)
                    formData.append("timestamp", Date.now().toString()) // 防止走缓存
                    try {
                        axios.post("http://127.0.0.1:3001/upload/large", formData, {
                            headers: {
                                "content-type": "multipart/form-data"
                            }
                        })
                    } catch (error) {
                        return Promise.reject(error)
                        throw error
                    }
                }
            })
        }
        // 判断文件是否存在
        const juedgeFileExist = async (file, md5) => {
            try {
                const response = await axios.post("http://127.0.0.1:3001/upload/exists", formData, {
                    params: {
                        "name": file.nam,
                        md5,
                    }
                })
                return response.data.data
            } catch (error) {
                return {}
                throw error
            }
        }
        // 合并请求
        const concatChunkFile = (file, md5) => {
            try {
                axios.post("http://127.0.0.1:3001/upload/concatFiles", {
                    "name": file.nam,
                    md5,
                })
            } catch (error) {
                throw error
            }
        }
        // 把要发送的函数放在队列里,每次从头部取一个函数调用,这样就可以控制并发数量
        const asyncPool = (chunkListObj, chunkIds) => {
            return new Promise((resolve,reject) => {
                requestQueue.push(...uploadChunkFile(chunkListObj, chunkIds))
                run(resolve,reject)
            })
        }
        const run = (resolve,reject) => {
            while(currentRequest < maxRequest && requestQueue.length > 0){
                const task = requestQueue.shift()
                currentRequest++
                task().then().finally(() => {
                    currentRequest--
                    run(resolve,reject)
                })
            }
            if(currentRequest === 0 && requestQueue.length === 0) {
                resolve()
            }
        }
    </script>
</body>

</html>

优化

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

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

相关文章

LabVIEW与PLC交互

一、写法 写命令立即读出 写命令后立即读出&#xff0c;在同一时间不能有多个地方写入&#xff0c;因此需要在整个写入后读出过程加锁 项目中会存在多个循环并行执行该VI&#xff0c;轮询PLC指令 在锁内耗时&#xff0c;就是TCP读写的实际耗时为5-8ms&#xff0c;在主VI六个…

Selenium记录RPA初阶 - 基本输入元件

防止自己遗忘&#xff0c;故作此为记录。 爬取网页基本元件并修改后爬取。 包含元件&#xff1a; elements: dict[str, str] {"username": None,"password": None,"email": None,"website": None,"date": None,"ti…

第三个Qt开发实例:利用之前已经开发好的LED驱动在Qt生成的界面中控制LED2的亮和灭

前言 上一篇博文 https://blog.csdn.net/wenhao_ir/article/details/145459006 中&#xff0c;我们是直接利用GPIO子系统控制了LED2的亮和灭&#xff0c;这篇博文中我们利用之前写好的LED驱动程序在Qt的生成的界面中控制LED2的亮和灭。 之前已经在下面两篇博文中实现了LED驱动…

Android studio 创建aar包给Unity使用

1、aar 是什么&#xff1f; 和 Jar有什么区别 aar 和 jar包 都是压缩包&#xff0c;可以使用压缩软件打开 jar包 用于封装 Java 类及其相关资源 aar 文件是专门为 Android 平台设计的 &#xff0c;可以包含Android的专有内容&#xff0c;比如AndroidManifest.xml 文件 &#…

BurpSuite抓包与HTTP基础

文章目录 前言一、BurpSuite1.BurpSuite简介2.BurpSuite安装教程(1)BurpSuite安装与激活(2)安装 https 证书 3.BurpSuite使用4.BurpSuite资料 二、图解HTTP1.HTTP基础知识2.HTTP客户端请求消息3.HTTP服务端响应消息4.HTTP部分请求方法理解5.HTTPS与HTTP 总结 前言 在网络安全和…

把DeepSeek接入Word软件,给工作提质增效!

前几天给大家分享了 DeepSeek 的资源包&#xff0c;可能很多人并没有本地部署 DeepSeek 的需求&#xff0c;只想使用它来提高一下工作效率。那今天来分享一下怎么直接在 Word 软件调用 DeepSeek&#xff0c;避免在 Word 软件和网页版 DeepSeek 里来回切换。 ## 前置条件 1、有…

Linux进阶——web服务器

一、相关名词解释及概念&#xff1a; www&#xff1a;(world wide web)全球信息广播&#xff0c;通常来说的上网就是使用www来查询用户所需的信息。使用http超文本传输协议。 过程&#xff1a;web浏览器向web服务&#xff08;Apache&#xff0c;Microsoft&#xff0c;nginx&…

QT笔记——多语言翻译

文章目录 1、概要2、多语言切换2.1、结果展示2.2、创建项目2.2、绘制UI2.2、生成“.st”文件2.4、生成“.qm”文件2.5、工程demo 1、概要 借助QT自带的翻译功能&#xff0c;实现实际应用用进行 “多语言切换” 2、多语言切换 2.1、结果展示 多语言切换 2.2、创建项目 1、文件…

oracle 基础语法复习记录

Oracle SQL基础 因工作需要sql能力&#xff0c;需要重新把sql这块知识重新盘活&#xff0c;特此记录学习过程。 希望有新的发现。加油&#xff01;20250205 学习范围 学习SQL基础语法 掌握SELECT、INSERT、UPDATE、DELETE等基本操作。 熟悉WHERE、GROUP BY、ORDER BY、HAVIN…

网络工程师 (22)网络协议

前言 网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合&#xff0c;它规定了通信时信息必须采用的格式和这些格式的意义。 一、基本要素 语法&#xff1a;规定信息格式&#xff0c;包括数据及控制信息的格式、编码及信号电平等。这是协议的基础&#xff0c;确…

【银河麒麟高级服务器操作系统】系统日志Call trace现象分析及处理全流程

了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer.kylinos.cn 文档中心&#xff1a;https://document.kylinos.cn 服务器环境以及配置 系统环境 物理机/虚拟机/云…

Milvus 存储设计揭秘:从数据写入到 Segment 管理的全链路解析

作为一款云原生向量数据库&#xff0c;Milvus 的高效查询性能有赖于其独特的存储架构设计。然而&#xff0c;在实际使用过程中&#xff0c;许多社区用户常常会遇到以下问题&#xff1a; 为什么频繁调用 flush 后&#xff0c;查询速度会变慢&#xff1f; 数据删除后&#xff0c;…

Redis双写一致性(数据库与redis数据一致性)

一 什么是双写一致性&#xff1f; 当修改了数据库&#xff08;MySQL&#xff09;中的数据&#xff0c;也要同时更新缓存&#xff08;redis&#xff09;中的数据&#xff0c;缓存中的数据要和数据库中的数据保持一致 双写一致性&#xff0c;根据业务对时间上的要求&#xff0c;…

14.PPT:中国注册税务师协会宣传【26】

目录 NO12 NO3/4/5​ NO678​ 【文本框水平/垂直居中】【文本框内容水平/垂直居中】 NO12 坑&#xff1a;注意❗Word文档的PPt素材.docx的标题大纲是混乱的&#xff0c;虽然他设置了&#xff0c;所以我们需要重新设置 设计→主题视图→幻灯片母版→删除版式插入logo NO3/4…

搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程

参考文章&#xff1a; 安装protoc、protoc-gen-go、protoc-gen-go-grpc-CSDN博客 一、简单介绍 本文开发环境&#xff0c;均为 windows 环境&#xff0c;mac 环境其实也类似 ~ ① 编译proto文件&#xff0c;相关插件 简单介绍&#xff1a; protoc 是编译器&#xff0c;用于将…

autMan奥特曼机器人-对接deepseek教程

一、安装插件ChatGPT 符合openai api协议的大模型均可使用此插件&#xff0c;包括chatgpt-4/chatgpt-3.5-turbo&#xff0c;可自定义服务地址和模型&#xff0c;指令&#xff1a;gpt&#xff0c;要求Python3.7以上&#xff0c;使用官方库https://github.com/openai/openai-pyt…

数据分析:企业数字化转型的金钥匙

引言&#xff1a;数字化浪潮下的数据金矿 在数字化浪潮席卷全球的背景下&#xff0c;有研究表明&#xff0c;只有不到30%的企业能够充分利用手中掌握的数据&#xff0c;这是否让人深思&#xff1f;数据已然成为企业最为宝贵的资产之一。然而&#xff0c;企业是否真正准备好从数…

Spring Web MVC项目的创建及使用

一、什么是Spring Web MVC&#xff1f; Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架&#xff0c;从⼀开始就包含在 Spring 框架中&#xff0c;通常被称为Spring MVC。 1.1 MVC的定义 MVC 是 Model View Controller 的缩写&#xff0c;它是软件工程中的一种软件架构…

MySQL的底层原理与架构

前言 了解MySQL的架构和原理对于很多的后续很多的操作会有很大的帮助与理解。并且很多知识都与底层架构相关联。 了解MySQL架构 通过上面的架构图可以得知&#xff0c;Server层中主要由 连接器、查询缓存、解析器/分析器、优化器、执行器 几部分组成的&#xff0c;下面将主要…

Node.js 实现简单爬虫

介绍 爬虫是一种按照一定的规则&#xff0c;自动地抓取万维网信息的程序或者脚本。 本文将使用 Nodejs 编写一个简单的爬虫脚本&#xff0c;爬取一个美食网站&#xff0c;获取菜品的标题和图片链接&#xff0c;并以表格的形式输出。 准备工作 1、初始化项目 首先&#xff0…