vue+element+md5实现大文件分片上传、断点续传

概述:

一、上传组件部分:使用el-upload,在页面创建一个上传控件。

        UI组件参数:multiple--是否支持多选文件、action--必选参数,上传的地址(因为实际情况下肯定会使用项目组件来调用后端接口,所以此处我给了action="",)、show-file-list--是否显示已上传文件列表、http-request--覆盖默认的上传行为,可以自定义上传的实现(一开始是使用before-upload来做上传文件之前的钩子,但是我们需要拦截action 属性的请求,做自定义文件的上传行为,官网上给出了“返回 false 或者返回 Promise 且被 reject,则停止上传”,但是我在return false后还是会在上传时默认调用一次空请求,再调用我的自定义上传请求接口,没办法只能将before-upload替换为http-request实现自定义上传)、limit--配合multiple使用,最大允许上传个数(这里有个坑要注意一下,上传后你会发现on-exceed的返回参数中,上传文件数是不断累积的,比如你限制最大同时上传10个文件,第一次你上传了7个,第二次你上传了5个,此时会提示你上传了12个,因此第二次被限制住了,所以需要在on-success中清除上传历史记录,防止文件限制莫名其妙的超出)、on-exceed--文件超出个数限制时的钩子、on-success--文件上传成功时的钩子。

<el-upload ref="upload" multiple action="" :show-file-list="false" :http-request="handleFileUpload" :limit="10" :on-exceed="handleExceed" :on-success="handleAvatarSuccess">

              <Button class="button" type="primary" :disabled="disabled">上传</Button>

</el-upload>

 二、JS部分:点击上传后触发自定义的上传方法,接收一个file参数为上传的文件,这里要注意两点,1.当同时上传多个文件时,会多次触发事件,有几个文件就执行几次方法,file参数为单次执行的文件对象而不是全部的上传文件;2.before-upload的返回参数直接就是文件对象,而http-request的返回参数需要file.flie才能到文件对象。

        首先通过file.size获得文件大小,判断当前文件是否需要分片上传,过小的文件直接上传即可,此分支不再多讲。

        npm安装spark-md5

npm install --save spark-md5

         在当前文件引入

import SparkMD5 from "spark-md5";

        为当前文件生成md5值(md5主要是给断点续传和快传用的,就是用来做个标识,具体原理这里就不讲了,只写前端业务实现。)正常是判断文件大小,如果不是很大的情况直接用整个文件生成md5值就行,当判断文件过大时,应该先用file.slice()分割文件,将第一片的md5值拼接最后一片的md5值,防止性能消耗过大。我这里就简单的和后端沟通一下,统一不做区分,直接全部用第一片的拼接最后一片的md5值。

        文件完整性检测

        在正式调用上传接口前先调用文件完整性检测接口,为断点续传用,后端通过md5值结合文件名来检测当前文件是否已经上传过,上传了多少,获取当前文件应该上传的分片的下标。

        上传文件

        file.slice()分割出本次上传的文件,同各种业务参数调用上传接口上传文件。在成功的回调中递归调用上传方法,每次重新分割下一片文件,反复上传直到全部上传完毕。(代码包含进度条部分)

    // 上传(同时选择多个文件时会多次上传该文件)
    async handleFileUpload(item) {
      let file = item.file
      this.disabled = true;
      this.list = [...this.list, file]; //用于展示进度
      this.uploadModal = true; // 展示进度弹窗
      // 文件大于50MB时分片上传
      if(file.size / 1024 / 1024 < 50){
        const formData = new FormData();
        formData.append("file", file);
        formData.append("name", file.name);
        this.$api.xxx(formData).then((res) => {
          if (res.error !== "error") {
            this.list.forEach((item)=>{
              if(item.name === file.name){
                item.percent = 100;
              }
            })
            this.$Message.success(`${file.name}:上传完成`);
          }else {
            this.list.forEach((item)=>{
              if(item.name === file.name){
                item.typeProgress = 1;
              }
            })
          }
        }).finally(() => {
          this.disabled = false;
        });
      }else{
        const size = 10 * 1024 * 1024; // 10MB 每个分片大小
        let current = 0; // 当前分片index(从0开始)
        let total = Math.ceil(file.size / size); // 分片总数
        let startByte = 0;
        let that = this;
        // 通过文件获取对应的md5值
        let dataFileStart = file.slice(0, size); // 第一片文件
        let dataFileEnd = file.slice(size* (total - 1) , file.size) // 最后一片文件
        var spark = new SparkMD5.ArrayBuffer();
        function getMd5(file) {
          return new Promise((resolve) => {
            // 对文件对象的处理
            var fileReader = new FileReader();
            fileReader.readAsArrayBuffer(file);
            // fileReader.onload为异步函数,要放到Promise对象中,等待状态的变更后再返回生成的md5值
            fileReader.onload = function (e) {
              spark.append(e.target.result);
              resolve(spark.end());
            };
          });
        }
        //获取文件二进制数据
        let md5Start = await getMd5(dataFileStart); // 第一片的md5值
        let md5End = await getMd5(dataFileEnd); // 最后一片的md5值
        let md5 = md5Start + md5End;
        // 文件完整性检测
        this.$api.xxx({md5:md5,fileName:file.name}).then((res) => {
          if (res.error !== "error") {
            current = res?.indexOf("0") === -1?0 : res?.indexOf("0"); // 当前服务器应该上传的分片下标
            startByte = size* current
            uploadChunk();
          }
        }).finally(() => {
          this.disabled = false;
        });
        // 编辑上传参数并上传文件
        function uploadChunk() {
          const formData = new FormData();
          const endByte = Math.min(startByte + size, file.size);
          const chunk = file.slice(startByte, endByte); // 当前分片文件
          formData.append("file", chunk);
          formData.append("name", file.name);
          formData.append("current", current);
          formData.append("total", total);
          formData.append("md5", md5);
          that.$api.xxx(formData).then((res) => {
            if (res.error !== "error") {
              that.list.forEach((item)=>{
                if(item.name === file.name){
                  item.percent = Math.floor(((Number(current) + 1) * 100) / total);
                }
              })
              that.list= [...that.list]
              startByte = endByte;
              if (startByte < file.size) {
                current++;
                uploadChunk();
              } else {
                that.$Message.success(`${file.name}:上传完成`);
              }
            }else {
              that.list.forEach((item)=>{
                if(item.name === file.name){
                  item.typeProgress = 1;
                }
              })
            }
          }).finally(() => {
            that.disabled = false;
          });
        }
      }
      return false;
    },
    // 文件超出个数限制时的钩子
    handleExceed(files) {
      this.$Message.warning(`最多同时上传 10 个文件,本次选择了 ${files.length} 个文件`);
    },
    // 文件上传成功时的钩子(清除上传历史记录,防止文件限制超出)
    handleAvatarSuccess(){
      this.$refs.upload.clearFiles();
    }

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
断点续传是指在文件上传过程中,如果上传中断,可以从上传中断的地方开始继续上传,避免重新上传整个文件。在使用 VueElement、Axios 和 Qs 实现断点续传时,可以按照以下步骤进行操作: 1. 前端页面部分 在前端页面中,可以使用 Element 组件库来实现上传文件的功能。例如可以使用 el-upload 组件来上传文件,代码如下: ``` <template> <el-upload class="upload-demo" action="yourUploadUrl" :auto-upload="false" :on-change="handleChange" :data="uploadData" :file-list="fileList" :http-request="uploadFile" > <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" :disabled="!fileList.length" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </template> ``` 在模板中,el-upload 组件的属性值中,我们需要设置以下内容: - `action` 属性:设置上传文件的地址。 - `auto-upload` 属性:设置是否自动上传。 - `on-change` 属性:设置文件上传状态改变时的回调函数。 - `data` 属性:设置上传文件时需要携带的参数。 - `file-list` 属性:设置已经上传文件列表。 - `http-request` 属性:设置文件上传的函数,我们在这里定义上传文件的逻辑。 接下来,我们需要在 data 函数中定义 fileList 和 uploadData 对象,代码如下: ``` data() { return { fileList: [], uploadData: { chunkSize: 1024 * 1024, // 文件切片大小 chunks: 0, // 切片总数 chunkIndex: 0, // 当前切片编号 fileSize: 0, // 文件大小 fileName: '', // 文件名 fileMd5: '', // 文件md5值 uploadUrl: '', // 上传接口地址 }, } }, ``` 在这里,我们需要定义上传文件时需要携带的参数。其中,chunkSize 表示每个切片的大小,chunks 表示总共需要切片的数量,chunkIndex 表示当前上传的切片编号,fileSize 表示文件的大小,fileName 表示文件的名称,fileMd5 表示文件md5 值,uploadUrl 表示上传接口的地址。 然后,我们需要在 methods 函数中定义 handleChange、uploadFile 和 submitUpload 函数。其中,handleChange 函数用来监听文件上传状态的改变,uploadFile 函数用来上传文件,submitUpload 函数用来提交上传请求。代码如下: ``` methods: { handleChange(file) { this.fileList = [file] this.uploadData.fileSize = file.size this.uploadData.fileName = file.name this.uploadData.fileMd5 = 'md5' this.uploadData.uploadUrl = 'uploadUrl' this.uploadData.chunks = Math.ceil(file.size / this.uploadData.chunkSize) }, async uploadFile(file) { const index = this.uploadData.chunkIndex++ const startByte = index * this.uploadData.chunkSize const endByte = Math.min((index + 1) * this.uploadData.chunkSize, this.uploadData.fileSize) const chunkFile = file.slice(startByte, endByte) const formData = new FormData() formData.append('file', chunkFile) formData.append('chunkIndex', index) formData.append('fileName', this.uploadData.fileName) formData.append('fileMd5', this.uploadData.fileMd5) const { data } = await this.$axios.post(this.uploadData.uploadUrl, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) if (index === this.uploadData.chunks - 1) { console.log('upload success') } else { this.uploadFile(file) } }, async submitUpload() { if (!this.fileList.length) return this.uploadData.chunkIndex = 0 await this.uploadFile(this.fileList[0].raw) } }, ``` 在这里,handleChange 函数将上传文件的基本信息保存到 uploadData 对象中,uploadFile 函数用来上传文件,submitUpload 函数用来提交上传请求。其中,uploadFile 函数是核心部分,它通过循环上传每个切片,如果上传成功,则继续上传下一个切片,否则重新上传当前切片。 2. 后端接口部分 在后端接口中,需要实现文件上传的逻辑。由于是断点续传,所以需要实现上传切片、合并切片的功能。例如可以使用 Node.js 和 Express 框架来实现上传文件的功能,代码如下: ``` const express = require('express') const multer = require('multer') const path = require('path') const fs = require('fs') const app = express() app.use(express.static('public')) app.post('/upload', multer({ dest: 'uploads/' }).single('file'), (req, res) => { const { fileName, fileMd5, chunkIndex } = req.body const chunkFile = req.file const chunkDir = path.join(__dirname, `./uploads/${fileMd5}`) const filePath = path.join(chunkDir, `${fileName}-${chunkIndex}`) if (!fs.existsSync(chunkDir)) { fs.mkdirSync(chunkDir) } fs.renameSync(chunkFile.path, filePath) res.json({ code: 0, message: '上传成功', }) }) app.post('/merge', (req, res) => { const { fileName, fileMd5, chunks } = req.body const chunkDir = path.join(__dirname, `./uploads/${fileMd5}`) const filePath = path.join(chunkDir, fileName) const chunkPaths = [] for (let i = 0; i < chunks; i++) { chunkPaths.push(path.join(chunkDir, `${fileName}-${i}`)) } let ws = fs.createWriteStream(filePath) chunkPaths.forEach((chunkPath) => { let rs = fs.createReadStream(chunkPath) rs.on('end', () => { fs.unlinkSync(chunkPath) }) rs.pipe(ws, { end: false }) }) ws.on('close', () => { fs.rmdirSync(chunkDir) res.json({ code: 0, message: '上传成功', }) }) }) app.listen(3000, () => { console.log('server is running at http://localhost:3000') }) ``` 在这里,我们实现了两个接口,一个是上传切片的接口 `/upload`,一个是合并切片的接口 `/merge`。其中,上传切片的接口会将上传的切片保存到指定的目录下,并返回上传成功的消息;合并切片的接口会将上传的所有切片合并成一个完整的文件,并删除上传的切片。注意,在合并切片的过程中,需要使用 fs.createWriteStream 和 fs.createReadStream 将切片合并成一个完整的文件。 以上就是使用 VueElement、Axios 和 Qs 实现文件上传断点续传的完整流程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值