element-plus实现前端分片上传的操作

24778次阅读 1894人点赞 作者: WuBin 发布时间: 2022-05-12 10:33:05
扫码到手机查看

准备工作

分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,原则就是化整为零,将大文件进行分片处理,切割成若干小文件,随后为每个分片创建一个新的临时文件来保存其内容,待全部分片上传完毕后,后端再按顺序读取所有临时文件的内容,将数据写入新文件中,最后将临时文件再删掉。

其实现在市面上有很多前端的三方库都集成了分片上传的功能,比如百度的WebUploader,遗憾的是它已经淡出历史舞台,无人维护了。现在比较推荐主流的库是vue-simple-uploader,不过饿了么公司开源的elementUI市场占有率还是非常高的。

首先前端需要安装需要的库:

npm install element-plus --save  
npm install spark-md5 --save  
npm install axios --save

随后在入口文件main.js中进行配置:

import { createApp } from 'vue';
import App from './App.vue';

import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

createApp(App)
    .use(ElementPlus)
    .mount('#app');

配置好之后,设计方案,前端通过elementUI上传时,通过分片大小的阈值对文件进行切割,并且记录每一片文件的切割顺序(chunk),在这个过程中,通过SparkMD5来计算文件的唯一标识(防止多个文件同时上传的覆盖问题identifier),在每一次分片文件的上传中,会将分片文件实体,切割顺序(chunk)以及唯一标识(identifier)异步发送到后端接口(fastapi),后端将chunk和identifier结合在一起作为临时文件写入服务器磁盘中,当前端将所有的分片文件都发送完毕后,最后请求一次后端另外一个接口,后端将所有文件合并。

具体实现

前端实现

根据方案,前端建立chunkupload.js文件:

import SparkMD5 from 'spark-md5'

//错误信息  
function getError(action, option, xhr) {  
    let msg  
    if (xhr.response) {  
        msg = `${xhr.response.error || xhr.response}`  
    } else if (xhr.responseText) {  
        msg = `${xhr.responseText}`  
    } else {  
        msg = `fail to post ${action} ${xhr.status}`  
    }  
    const err = new Error(msg)  
    err.status = xhr.status  
    err.method = 'post'  
    err.url = action  
    return err  
}  
// 上传成功完成合并之后,获取服务器返回的json  
function getBody(xhr) {  
    const text = xhr.responseText || xhr.response  
    if (!text) {  
        return text  
    }  
    try {  
        return JSON.parse(text)  
    } catch (e) {  
        return text  
    }  
}  
  
// 分片上传的自定义请求,以下请求会覆盖element的默认上传行为  
export default function upload(option) {  
    if (typeof XMLHttpRequest === 'undefined') {  
        return  
    }  
    const spark = new SparkMD5.ArrayBuffer()// md5的ArrayBuffer加密类  
    const fileReader = new FileReader()// 文件读取类  
    const action = option.action // 文件上传上传路径  
    const chunkSize = 1024 * 1024 * 1 // 单个分片大小,这里测试用1m  
    let md5 = ''// 文件的唯一标识  
    const optionFile = option.file // 需要分片的文件  
    let fileChunkedList = [] // 文件分片完成之后的数组  
    const percentage = [] // 文件上传进度的数组,单项就是一个分片的进度  
  
    // 文件开始分片,push到fileChunkedList数组中, 并用第一个分片去计算文件的md5  
    for (let i = 0; i < optionFile.size; i = i + chunkSize) {  
        const tmp = optionFile.slice(i, Math.min((i + chunkSize), optionFile.size))  
        if (i === 0) {  
            fileReader.readAsArrayBuffer(tmp)  
        }  
        fileChunkedList.push(tmp)  
    }  
  
    // 在文件读取完毕之后,开始计算文件md5,作为文件唯一标识  
    fileReader.onload = async (e) => {  
        spark.append(e.target.result)  
        md5 = spark.end() + new Date().getTime()  
        console.log('文件唯一标识--------', md5)  
        // 将fileChunkedList转成FormData对象,并加入上传时需要的数据  
        fileChunkedList = fileChunkedList.map((item, index) => {  
            const formData = new FormData()  
            if (option.data) {  
                // 额外加入外面传入的data数据  
                Object.keys(option.data).forEach(key => {  
                    formData.append(key, option.data[key])  
                })  
                // 这些字段看后端需要哪些,就传哪些,也可以自己追加额外参数  
                formData.append(option.filename, item, option.file.name)// 文件  
                formData.append('chunkNumber', index + 1)// 当前文件块  
                formData.append('chunkSize', chunkSize)// 单个分块大小  
                formData.append('currentChunkSize', item.size)// 当前分块大小  
                formData.append('totalSize', optionFile.size)// 文件总大小  
                formData.append('identifier', md5)// 文件标识  
                formData.append('filename', option.file.name)// 文件名  
                formData.append('totalChunks', fileChunkedList.length)// 总块数  
            }  
            return { formData: formData, index: index }  
        })  
  
        // 更新上传进度条百分比的方法  
        const updataPercentage = (e) => {  
            let loaded = 0// 当前已经上传文件的总大小  
            percentage.forEach(item => {  
                loaded += item  
            })  
            e.percent = loaded / optionFile.size * 100  
            option.onProgress(e)  
        }  
  
        // 创建队列上传任务,limit是上传并发数,默认会用两个并发  
        function sendRequest(chunks, limit = 2) {  
            return new Promise((resolve, reject) => {  
                const len = chunks.length  
                let counter = 0  
                let isStop = false  
                const start = async () => {  
                    if (isStop) {  
                        return  
                    }  
                    const item = chunks.shift()  
                    console.log()  
                    if (item) {  
                        const xhr = new XMLHttpRequest()  
                        const index = item.index  
                        // 分片上传失败回调  
                        xhr.onerror = function error(e) {  
                            isStop = true  
                            reject(e)  
                        }  
                        // 分片上传成功回调  
                        xhr.onload = function onload() {  
                            if (xhr.status < 200 || xhr.status >= 300) {  
                                isStop = true  
                                reject(getError(action, option, xhr))  
                            }  
                            if (counter === len - 1) {  
                                // 最后一个上传完成  
                                resolve()  
                            } else {  
                                counter++  
                                start()  
                            }  
                        }  
                        // 分片上传中回调  
                        if (xhr.upload) {  
                            xhr.upload.onprogress = function progress(e) {  
                                if (e.total > 0) {  
                                    e.percent = e.loaded / e.total * 100  
                                }  
                                percentage[index] = e.loaded  
                                console.log(index)  
                                updataPercentage(e)  
                            }  
                        }  
                        xhr.open('post', action, true)  
                        if (option.withCredentials && 'withCredentials' in xhr) {  
                            xhr.withCredentials = true  
                        }  
                        const headers = option.headers || {}  
                        for (const item in headers) {  
                            if (headers.hasOwnProperty(item) && headers[item] !== null) {  
                                xhr.setRequestHeader(item, headers[item])  
                            }  
                        }  
                        // 文件开始上传  
                        xhr.send(item.formData);  
                    }  
                }  
                while (limit > 0) {  
                    setTimeout(() => {  
                        start()  
                    }, Math.random() * 1000)  
                    limit -= 1  
                }  
            })  
        }  
  
        try {  
            // 调用上传队列方法 等待所有文件上传完成  
            await sendRequest(fileChunkedList,2)  
            // 这里的参数根据自己实际情况写  
            const data = {  
                identifier: md5,  
                filename: option.file.name,  
                totalSize: optionFile.size  
            }  
            // 给后端发送文件合并请求  
            const fileInfo = await this.axios({  
                method: 'post',  
                url: 'http://localhost:8000/mergefile/',  
                data: this.qs.stringify(data)  
            }, {  
                 headers: {  
                        "Content-Type": "multipart/form-data"  
                    }  
            }).catch(error => {  
                console.log("ERRRR:: ", error.response.data);  
  
            });  
  
            console.log(fileInfo);  
  
            if (fileInfo.data.code === 200) {  
                const success = getBody(fileInfo.request)  
                option.onSuccess(success)  
                return  
            }  
        } catch (error) {  
            option.onError(error)  
        }  
    }  
}

之后建立upload.vue模板文件,并且引入自定义上传控件:

<template>  
  <div>  
  <el-upload  
    :http-request="chunkUpload"  
    :ref="chunkUpload"  
    :action="uploadUrl"  
    :data="uploadData"  
    :on-error="onError"  
    :before-remove="beforeRemove"  
    name="file" >  
    <el-button size="small" type="primary">点击上传</el-button>  
  </el-upload>  
</div>  
</template>  
<script>  
//js部分  
import chunkUpload from './chunkUpload'  
export default {  
  data() {  
    return {  
      uploadData: {  
        //这里面放额外携带的参数  
      },  
      //文件上传的路径  
      uploadUrl: 'http://localhost:8000/uploadfile/', //文件上传的路径  
      chunkUpload: chunkUpload // 分片上传自定义方法,在头部引入了  
    }  
  },  
  methods: {  
    onError(err, file, fileList) {  
      this.$store.getters.chunkUploadXhr.forEach(item => {  
        item.abort()  
      })  
      this.$alert('文件上传失败,请重试', '错误', {  
        confirmButtonText: '确定'  
      })  
    },  
    beforeRemove(file) {  
      // 如果正在分片上传,则取消分片上传  
      if (file.percentage !== 100) {  
        this.$store.getters.chunkUploadXhr.forEach(item => {  
          item.abort()  
        })  
      }  
    }  
  }  
}  
</script>  
这里注意,请务必使用:http-request="chunkUpload" 覆盖原有的上传方法,http-request作用就是使用自定义的上传方式覆盖默认的。

这里定义的后端上传接口是:http://localhsot:8000/uploadfile/合并文件接口是:http://localhsot:8000/mergefile/

后端实现

前端搞定了,下面我们来编写接口,后端的任务相对简单,接收分片文件、分片顺序以及唯一标识,并且将文件临时写入到服务器中,当最后一个分片文件完成上传后,第二个接口负责按照分片顺序合并所有文件,合并成功后再删除临时文件,用来节约空间。

具体我的实现方法与此处不同,我是采用一个接口,将上传列表一定要改为串行,因为并行可能有后发先至的问题,所以我采用串行,当上传的当前片数=总片数的时候,进行合并。不过我觉得本文的实现方式更加的好。

相关资料

点赞 支持一下 觉得不错?客官您就稍微鼓励一下吧!
关键词:element-plus,切片上传
推荐阅读
  • uniapp实现被浏览器唤起的功能

    当用户打开h5链接时候,点击打开app若用户在已经安装过app的情况下直接打开app,若未安装过跳到应用市场下载安装这个功能在实现上主要分为两种场景,从普通浏览器唤醒以及从微信唤醒。

    9119次阅读 589人点赞 发布时间: 2022-12-14 16:34:53 立即查看
  • Vue

    盘点Vue2和Vue3的10种组件通信方式

    Vue中组件通信方式有很多,其中Vue2和Vue3实现起来也会有很多差异;本文将通过选项式API组合式API以及setup三种不同实现方式全面介绍Vue2和Vue3的组件通信方式。

    3884次阅读 289人点赞 发布时间: 2022-08-19 09:40:16 立即查看
  • JS

    几个高级前端常用的API

    推荐4个前端开发中常用的高端API,分别是MutationObserver、IntersectionObserver、getComputedstyle、getBoundingClientRect、requ...

    14139次阅读 919人点赞 发布时间: 2021-11-11 09:39:54 立即查看
  • PHP

    【正则】一些常用的正则表达式总结

    在日常开发中,正则表达式是非常有用的,正则表达式在每个语言中都是可以使用的,他就跟JSON一样,是通用的。了解一些常用的正则表达式,能大大提高你的工作效率。

    12947次阅读 445人点赞 发布时间: 2021-10-09 15:58:58 立即查看
  • 【中文】免费可商用字体下载与考证

    65款免费、可商用、无任何限制中文字体打包下载,这些字体都是经过长期验证,经得住市场考验的,让您规避被无良厂商起诉的风险。

    11539次阅读 926人点赞 发布时间: 2021-07-05 15:28:45 立即查看
  • Vue

    Vue3开发一个v-loading的自定义指令

    在vue3中实现一个自定义的指令,有助于我们简化开发,简化复用,通过一个指令的调用即可实现一些可高度复用的交互。

    15646次阅读 1248人点赞 发布时间: 2021-07-02 15:58:35 立即查看
  • JS

    关于手机上滚动穿透问题的解决

    当页面出现浮层的时候,滑动浮层的内容,正常情况下预期应该是浮层下边的内容不会滚动;然而事实并非如此。在PC上使用css即可解决,但是在手机端,情况就变的比较复杂,就需要禁止触摸事件才可以。

    14843次阅读 1209人点赞 发布时间: 2021-05-31 09:25:50 立即查看
  • Vue

    Vue+html2canvas截图空白的问题

    在使用vue做信网单页专题时,有海报生成的功能,这里推荐2个插件:一个是html2canvas,构造好DOM然后转canvas进行截图;另外使用vue-canvas-poster(这个截止到2021年3月...

    29040次阅读 2281人点赞 发布时间: 2021-03-02 09:04:51 立即查看
  • Vue

    vue-router4过度动画无效解决方案

    在初次使用vue3+vue-router4时候,先后遇到了过度动画transition进入和退出分别无效的情况,搜遍百度没没找到合适解决方法,包括vue-route4有一些API都进行了变化,以前的一些操...

    25064次阅读 1931人点赞 发布时间: 2021-02-23 13:37:20 立即查看
交流 收藏 目录