vue-simple-uploader组件的一些使用心得

前言

因为项目需要上传大文件,考虑使用分片上传、断点续传这些功能故选用vue-simple-uploader,期间踩的一些坑及解决方法记录一下。

git文档链接

1.https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md
2.https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md

使用vue-simple-uploader

1.安装插件:

npm install vue-simple-uploader --save

2.main.js中初始化

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

3.在*.vue文件中使用
代码仅供参考

        <uploader 
          ref="uploader"
          class="avatar-uploader"
          :options="options"   
          :file-status-text="statusText"
          @file-added="onFileAdded"
          @file-success="onFileSuccess"
          @file-progress="onFileProgress"
          @file-error="onFileError"
        >
          <!-- :autoStart=true -->
          <!-- @file-removed="fileRemoved" -->
          <uploader-unsupport></uploader-unsupport>
          <uploader-btn
          :single=true
          title="(800M以上)"
          >大文件上传</uploader-btn>
        </uploader>

        <el-collapse-item v-for="f in fileList" :key="f.uid" :name="f.uid">
          <template slot="title">
            <el-col :span="6">
              <span class="pull-left name" :title="f.name">{{f.name}}</span>
              <span class="pull-left status" v-bind:class="{error: f.state === 2}" v-text="statusStr(f.state)"></span>
            </el-col>
            <el-col :span="6" v-if="f.progress < 100 && f.state !== 2">
              <el-progress class="progress" :text-inside="true" :stroke-width="15" :percentage="f.progress"></el-progress>
            </el-col>
            <i class="el-icon-my-close close-btn" title="删除" @click="delFile($event, f)"></i>
            <span v-show="f.showPP">
            <i class="el-icon-my-play play-btn" v-show='!f.isPlayOrPause' title="开始" @click="playFile($event, f)"></i>
            <i class="el-icon-my-pause pause-btn" v-show='f.isPlayOrPause' title="暂停" @click="pauseFile($event, f)"></i>
            </span>
          </template>
          <el-row ref="formWrapper">
            <form-upload :info.sync="formObj" :file.sync="f" @deleteFile="delFileByFileuid" @expand="expandCollapse"></form-upload>
          </el-row>
        </el-collapse-item>

4.data中参数定义

      options:{
        target:"/file/fdfs/multipart-upload/chunkUpload",//即分片上传的URL
        chunkSize: 10 * 1024 * 1024,//分片大小
        simultaneousUploads:3,//并发上传数,默认 3
        testChunks: true,//是否开启服务器分片校验
        checkChunkUploadedByResponse: function (chunk, res) {// 服务器分片校验函数,秒传及断点续传基础
        //需后台给出对应的查询分片的接口进行分片文件验证
          let objMessage = JSON.parse(res);//skipUpload、uploaded 需自己跟后台商量好参数名称
          if (objMessage.skipUpload) {
            return true;
          }
          return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
        },
        maxChunkRetries: 2, //最大自动失败重试上传次数
        headers: {//在header中添加token验证,Authorization请根据实际业务来
          getUploadHeaders()
        },
        processParams(params) {//自定义每一次分片传给后台的参数,params是该方法返回的形参,包含分片信息,
          //若不自定义则默认会把文件所有参数传给后台,自己可以通过接口查看插件本身传递的参数
          return {//返回一个对象,会添加到每一个分片的请求参数里面
            totalChunks: params.totalChunks,
            identifier:params.identifier,
            chunkNumber: params.chunkNumber,
            chunkSize: 10 * 1024 * 1024,
            //  这是我跟后台约定好的参数,可根据自己项目实际情况改变
          };
        },
      }

文件钩子的使用

主要讲一下几个文件上传钩子和实例方法的使用,由于官方文档写的也不是很清楚,踩了很多坑
代码仅供参考

    onFileAdded(file,fileList) {
      file.pause()
      if(file.getType()!='zip'){
        EventBus.$emit('alert.show', {type: 'warning', msg: `只能上传.zip格式的文件`});
        file.ignored = true//文件校验,不符规则的文件过滤掉
        // file.cancel()
        // return false;
      }
      else if(file.size<=800*1024*1024){
        EventBus.$emit('alert.show', {type: 'warning', msg: `请上传800M以上的文件`});
        file.ignored = true//文件过滤

      }
      else if(this.fileList.length >= 10){
        EventBus.$emit('alert.show', {type: 'warning', msg: `最多上传10份文件`});
        file.ignored = true//文件过滤
      }else{
        // 新增文件的时候触发,计算MD5
        this.myMD5(file);
      }
    },
    onFileSuccess(rootFile, file, response, chunk){
      //文件成功的时候触发
      let index = this.findFileById(file.uniqueIdentifier);
      let res = JSON.parse(response)
      if(res.result==="上传成功"){
        this.uploadingFileStr(res.path,getUploadHeaders()).then(ress=>{
          if(ress.data.msgCode===1){
            if(index > -1){
              this.fileList[index].id = ress.data.data.id;
              this.fileList[index].resName = file.name.replace(/\.\w+$/g, '');
              this.fileList[index].name = file.name;
              this.fileList[index].filePath = null;
              this.fileList[index].coverPath = null;
              this.fileList[index].progress = 100;
              this.fileList[index].status = "success";
              this.fileList[index].state = 1;
              this.fileList[index].isPlayOrPause=false;
              this.fileList[index].showPP = false;
              this.expandCollapse(file.uniqueIdentifier);
            }
          }else{
            EventBus.$emit('alert.show', {type: 'error', msg: res.result});
            if(index > -1){
              this.fileList[index].status = 'fail';
              this.fileList[index].state = 2;
              this.fileList[index].isPlayOrPause=false;
              this.fileList[index].showPP = false;
            }
          }
        })
      }
    },
    onFileError(rootFile, file, response, chunk){
      let res = JSON.parse(response)
      EventBus.$emit('alert.show', {type: 'error', msg: res.result});
      let index = this.findFileById(file.uniqueIdentifier);
      if(index > -1){
        this.fileList[index].status = 'fail';
        this.fileList[index].state = 2;
        this.fileList[index].isPlayOrPause=false;
        this.fileList[index].showPP = false;
      }
    },
    onFileProgress(rootFile, file, chunk){
      let index = this.findFileById(file.uniqueIdentifier),
        p = Math.round(file.progress()*100);
      if(index > -1){
        if(p < 100){
          this.fileList[index].progress = p;
        }
        this.fileList[index].status = file.status;
      }
    },
    myMD5(file) {//这里主要是使用MD5对文件做一个上传的查重
      let md5 = "";
      md5 = SparkMD5.hash(file.name);//业务需求以文件名作为加密
      let index = this.findFileById(md5);
      if(index==-1){
        file.uniqueIdentifier = md5;
        this.fileList.push({
          id: null,
          uid: file.uniqueIdentifier,
          filePath: '',
          coverPath: '',
          name: file.name,
          resName: '',
          progress: 0,
          state: 0,
          status: '',
          isPlayOrPause:true,
          showPP:true,
          elMD5:md5   //多余的参数可注释
        });
        //继续上传文件
        file.resume();
      }else{
        EventBus.$emit('alert.show', {type: 'warning', msg: `该文件已上传至列表,请勿重复上传`});
        file.ignored = true//文件过滤
      }
    },
    playFile(e,f){
      e.stopPropagation();
      f.isPlayOrPause=true
      const uploaderInstance = this.$refs.uploader.uploader
      let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)//兼容点击上传按钮
      uploaderInstance.fileList[index].resume();
    },
    pauseFile(e,f){
      e.stopPropagation();
      f.isPlayOrPause=false
      const uploaderInstance = this.$refs.uploader.uploader
      let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
      uploaderInstance.fileList[index].pause();
    },
uoloader实例的一些方法和钩子

1.@file-added="onFileAdded" file-added(file,filelist)相当于before-upload,即在上传文件之前对文件进行的操作,一般用来做文件验证。如果没有配置autoStart=false点击上传文件按钮之后文件会自动上传

autoStart {Boolean}
默认 true, 是否选择文件后自动开始上传。

我们要在onFileAdded()中通过pause()方法暂停文件上传

   onFileAdded(file,fileList) {
      file.pause()//用来暂停文件上传
      if(file.getType()!='zip'){//根据自己项目要求进行文件验证
        EventBus.$emit('alert.show', {type: 'warning', msg: `只能上传.zip格式的文件`});
        file.ignored = true//文件校验,不符规则的文件过滤掉
        // file.cancel()//清除文件
        // return false;
      }
  }

这里要讲的是file.cancel()file.ignored = true

先讲一下这个插件获取uploader实例的方法
const uploaderInstance = this.$refs.uploader.uploader
uploaderInstance 就是uploader实例

在项目中点击上传文件按钮(第一次上传),文件成功加入fileList之后,再次上传此文件(第二次),会进入onFileAdded()进行文件验证,因为我进行文件查重,重复的就不在上传了,所以报错之后使用uploader实例提供的file.cancel()方法清除文件,此后再有点击文件上传此文件都不会跳入onFileAdded()方法,更不会进行验证和报错,这对于用户来说无疑是一个巨大的BUG。
通过检查log发现,每次点击上传文件之后插件会在uploader实例中的files数组中加入当前文件的信息,用插件实例方法cancel()是清除不掉当前文件的。如果使用uploaderInstance.cancel()则会清除包括正在上传,暂停上传,已上传成功的所有的文件信息。使用file.ignored = true使验证失败的文件不加入uploader实例中的files数组,则下次点击上传文件按钮可再次调起onFileAdded()进行验证

uploader实例中的fileList数组中的文件才是实际正在上传文件列表,对于uploader实例中files,file,fileList这几个数组的具体作用有待研究

2.@file-success="onFileSuccess" 文件上传成功的回调

    onFileSuccess(rootFile, file, response, chunk){
      //文件成功的时候触发
    },

3.@file-progress="onFileProgress" 用来获取文件的实时上传进度

    onFileProgress(rootFile, file, chunk){
      let index = this.findFileById(file.uniqueIdentifier),//通过index来获取对应的文件progress
          p = Math.round(file.progress()*100);
      if(index > -1){
        if(p < 100){
          this.fileList[index].progress = p;
        }
        this.fileList[index].status = file.status;
      }
    },

4.@file-error="onFileError" 文件上传失败的回调

    onFileError(rootFile, file, response, chunk){
      //文件上传失败的回调
    },

5.@file-removed="fileRemoved" 删除文件的回调
也可自定义删除的按钮不使用此钩子

  delFile(e, f){
      e.stopPropagation();
      let txt = '是否删除选中数据?';
      if(f.id){
        txt = '该资源还未提交信息,确认删除选中数据?';
      }
      this.$confirm(txt, '删除', {
        type: 'warning',
        showCancelButton: false
      }).then(() => {
        const uploaderInstance = this.$refs.uploader.uploader
        let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
        if(index>-1){
          uploaderInstance.fileList[index].cancel();   //这句代码是删除所选上传文件的关键
        }
        this.delFileByFileuid(f.uid);
      }).catch(console.error);
    },

6.文件的暂停上传和继续上传

    playFile(e,f){
      e.stopPropagation();
      f.isPlayOrPause=true
      const uploaderInstance = this.$refs.uploader.uploader
      let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)//兼容点击上传按钮
      uploaderInstance.fileList[index].resume();   //
    },
    pauseFile(e,f){
      e.stopPropagation();
      f.isPlayOrPause=false
      const uploaderInstance = this.$refs.uploader.uploader
      let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
      uploaderInstance.fileList[index].pause();   //
    },

在第5和第6点中对删除文件,暂停,继续上传都需要通过获取uploader实例中fileList对应的Index来进行操作
即:
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
uploaderInstance.fileList[index].pause();// resume();cancel();

7.关于断点续传

官方文档是这样介绍的
checkChunkUploadedByResponse 可选的函数用于根据 XHR 响应内容检测每个块是否上传成功了,传入的参数是:Uploader.Chunk 实例以及请求响应信息。这样就没必要上传(测试)所有的块了

      checkChunkUploadedByResponse: function (chunk, res) {// 服务器分片校验函数,秒传及断点续传基础
        //需后台给出对应的查询分片的接口进行分片文件验证
          let objMessage = JSON.parse(res);//skipUpload、uploaded 需自己跟后台商量好参数名称
          if (objMessage.skipUpload) {
            return true;
          }
          return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
        },

checkChunkUploadedByResponse()options中的一个配置项,配置之后上传文件之前会自动调起一个get请求获取已上传的分片信息。返回的内容自己跟后台协商,一般是返回已上传的分片的index数组,然后前端通过对返回的分片index进行筛选跳过已上传的分片,只上传未上传的分片。

  1. MD5文件验证
    断点续传及秒传的基础是要计算文件的MD5,这是文件的唯一标识,然后服务器根据MD5进行判断,我这里主要是用来做文件查重判断
    我这里使用的加密工具是spark-md5,可以通过npm来安装
npm install spark-md5 --save

在当前vue文件引用即可

import SparkMD5 from 'spark-md5';

file有个属性是uniqueIdentifier,代表文件唯一标示,我们把计算出来的MD5赋值给这个属性 file.uniqueIdentifier = md5

9.关于file.getType()的坑

getType()方法用于获取文件类型。
file.getType() ===>'zip' 等文件类型

如我在onFileAdd方法中做的文件格式验证

    if(file.getType()!='zip'){
        EventBus.$emit('alert.show', {type: 'warning', msg: `只能上传.zip格式的文件`});
        file.ignored = true
      }

但是项目提测之后发现验证文件格式老是出问题,检查log发现在我的电脑上file.getType()返回的是zip,在测试的电脑上返回的是x-zip-compressed。另外又试了其他格式的文件输出其文件格式如下图,发现两台电脑的zip格式文件返回的不一样。关键的问题就在于不同电脑上返回的文件格式不一样(有待研究该api),所以对于getType()的使用得根据自己项目来考虑自己获取还是使用该api
提供一个解决方案,截取file.name的值获取后缀

file.name.substring(file.name.lastIndexOf(".")+1) 

另外关于文件上传的格式application/x-zip-compressed,application/zip,application/vnd.ms-excel……有兴趣的小伙伴可以自行研究一下插件的底层代码

电脑1

电脑2

功能完成

图1

说明:兼容了el-upload的上传按钮,第4个文件是用el-upload按钮进行上传的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339

推荐阅读更多精彩内容