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

获取文件对象

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>

优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值