基于antd框架的vue + typescript 实现大文件分片上传

最近用vue+typescript搭的一个框架写项目,UI框架使用的是ant design for vue的,由于其中还用到了“vue-property-decorator” 和 “babel-plugin-jsx-v-model”等依赖,用以支持TS和JSX的写法,所以不熟悉的可能看起来比较懵。当然,代码形式不一样,思想是一样的,基础JS代码怎么说也是看得懂的,那就足够了。

先总结一下分片的思路吧:

1.先根据一定大小计算要把文件分成几块,利用FileReader对象读取上传的文件并分段截取成字节流数组;

2.若需要进行断点续传,则在截取完毕后还要同时根据文件的完整字节流生成对应的MD5值,用于对分片上传的文件进行标记归类,和查询当前片是否已传给后台(断点续传:已上传的文件片无需重复上传);

3.循环执行(检查当前片是否已上传、上传当前片)这些操作,直到全部片上传完毕;

4.向后台发送合并请求接口,由后台完成合并操作。

后端人员的代码是参考  Spring Boot[五]:WebUploader分片断点上传  这个写的,据他说有两种方式,一种是这个,生成临时文件最后合并,一种是在实体中拼接文件流(记不清有没有在最后的参考文章中记录了)。

现在直接上代码吧

1.JSX中引入antd的upload组件

<a-upload
          name="file"
          accept={this.videoUploadData.acceptType}
          multiple={false}
          action={this.uploadUrl}
          headers={this.headers}
          data={this.uploadData}
          fileList={this.fileList}
          beforeUpload={this.handleBeforeUpload}
          customRequest={this.handleUpload}
          remove={this.handleRemove}
          on-reject={this.handleReject}
          on-preview={this.handlePreview}
          on-change={this.handleChange}
        >
          {this.fileList.length >= this.videoUploadData.num ? (
            ''
          ) : (
            <div>
              <a-button style={this.style.btu}>
                {' '}
                <a-icon type="plus" style={this.style.icon} />
              </a-button>
            </div>
          )}
        </a-upload>

2.选择文件后,会先走beforeUpload方法,我们有初始化内容的可以写在这里

/**
   * @description 上传前
   * @author YXM
   */
  handleBeforeUpload(file: any, fileList: any) {
    this.successChunk = 0 // 重置当前已上传成功的片数
    this.chunkList = [] // 清空文件流数组
    // this.$AntMessage.loading({ duration: 0, content: '文件上传中...' })
  }

3.用自写方法代替原组件的上传方法,

/**
   * @description 手动请求上传服务
   * @author YXM
   */
  handleUpload(param: any) {
    const file = param.file // 组件提供的文件
    this.computeMD5(file).then((md5:any)=>{
      if (this.chunkList.length > 0) { // 判断字节流数组长度
        for(let i = 0,len = this.chunkList.length;i<len;i++) {

          // 这里请求checkFile,发送MD5返回是否已上传该片段,只不过我没写

          let formData = new FormData() // 按分片个数发送请求
          formData.append('file', this.chunkList[i])
          formData.append('md5File', md5)
          formData.append('chunk', i.toString()) // 属于第几片
          this.$Api
            ._postMultiData({
              url: this.$Api.apiModulesList.videoUpload.upload.url,
              method: 'post',
              headers: {
                token: Cookies.get('token'),
                'Content-Type': 'multipart/form-data; boundary=ABCD',
              },
              data: formData,
            })
            .then((res: any) => {
              if(res && res.status){
                this.successChunk++ // 记录当前已上传成功的片数
              } else {
                // this.$AntMessage.warning('上传异常')
              }
            })
        }
      }
      
    })
  }

/**
   * 计算md5,实现断点续传及秒传
   * @param file
   */
  computeMD5(file: any) {
    const _this = this
    this.filename = file.name
    return new Promise((resolve, reject)=>{
      try {
        let fileReader = new FileReader();
        let time = new Date().getTime();
        let blobSlice = File.prototype.slice
        // let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
        let currentChunk = 0; // 当前第几片
        const chunkSize = 5 * 1024 * 1024; // 每片的大小,这里是5M
        let chunks = Math.ceil(file.size / chunkSize); // 总片数
        let spark = new SparkMD5.ArrayBuffer();
        loadNext();
        fileReader.onload = ((e: any) => {
            spark.append(e.target.result);
            currentChunk++;
            if (currentChunk < chunks) {
                loadNext();
            } else {
                this.md5 = spark.end();
                resolve(this.md5)
                console.log(`MD5计算完毕:${file.name} \nMD5:${this.md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
            }
        });
        fileReader.onerror = function () {
            // this.error(`文件${file.name}读取出错,请检查该文件`)
            // file.cancel();
        };
        function loadNext() {
            let start = currentChunk * chunkSize;
            let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
            fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
            _this.chunkList.push(blobSlice.call(file, start, end)) // 将每次分片的字节流放到数组里
        }
      }catch(e) {
        reject(e)
      }
    })
  }

4.监听 记录已成功的接口次数successChunk 数据,如果和字节流数组长度相同,则表明所有的接口都执行成功了,则发送合并请求。

@Watch('successChunk')
  watchSuccessChunk(val: any) {
    if (val == this.chunkList.length) {
      this.sendMerge()
    }
  }

sendMerge(){
    this.successChunk = 0 // 重置
    this.$ModuleApis.videoUpload
    .merge({
      data: {
        baseId: this.uploadData.baseId,
        chunks: this.chunkList.length,
        md5File: this.md5,
        name: this.filename
      },
    })
    .then((res: any) => {
      if (res.code === window.CROSS_CODE) { // 返回200
        this.$AntMessage.success('上传成功')
        this.handleUploadSuccess()
      }
    })
  }

/**
   * @description 上传成功之后告诉父组件
   * @author YXM
   */
  @Emit('emitUploadSuccess')
  handleUploadSuccess() {}

这样,整个前端的任务就完成了。

实际代码有删改(自己加了进度条等),主要讲思路,各位根据自身需要添加,这里不再赘述。下面上全部代码

import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator'
import Cookies from 'js-cookie'
import SparkMD5 from 'spark-md5'
import './uploadVideo.scss' // css样式
@Component({
  name: 'uploadFilesComponents',
})
export default class uploadFilesComponents extends Vue {
  headers: any = {
    token: Cookies.get('token'),
  }

  fileList: any = []

  @Prop({ default: () => {} }) private videoUploadData!: any // 上传文件的众多参数

  @Prop({ default: '' }) private uploadUrl!: String // 上传的路径

  @Prop({ default: () => {} }) private uploadData?: any // 上传的额外参数

  chunkList:any = [] // 存放字节流数组

  filename:any = null // 文件名称

  successChunk:any = 0 // 已上传成功片数

  md5: any = null //加密值

  mounted() {}

  @Watch('successChunk')
  watchSuccessChunk(val: any) {
    if (val == this.chunkList.length) {
      this.sendMerge()
    }
  }

  render() {
    return (
      <div style={this.style.box}>
        <a-upload
          name="file"
          accept={this.videoUploadData.acceptType}
          multiple={false}
          action={this.uploadUrl}
          headers={this.headers}
          data={this.uploadData}
          fileList={this.fileList}
          beforeUpload={this.handleBeforeUpload}
          customRequest={this.handleUpload}
          remove={this.handleRemove}
          on-reject={this.handleReject}
          on-preview={this.handlePreview}
          on-change={this.handleChange}
        >
          {this.fileList.length >= this.videoUploadData.num ? (
            ''
          ) : (
            <div>
              <a-button style={this.style.btu}>
                {' '}
                <a-icon type="plus" style={this.style.icon} />
              </a-button>
            </div>
          )}
        </a-upload>
      </div>
    )
  }

  /**
   * @description 上传前
   * @author YXM
   */
  handleBeforeUpload(file: any, fileList: any) {
    this.successChunk = 0
    this.chunkList = []
    // this.$AntMessage.loading({ duration: 0, content: '文件上传中...' })
  }

  /**
   * @description 手动请求上传服务
   * @author YXM
   */
  handleUpload(param: any) {
    const file = param.file
    this.computeMD5(file).then((md5:any)=>{
      if (this.chunkList.length > 0) {
        for(let i = 0,len = this.chunkList.length;i<len;i++) {

          // 这里请求checkFile,发送MD5返回是否已上传该片段,只不过我没写


          let formData = new FormData()
          formData.append('file', this.chunkList[i])
          formData.append('md5File', md5)
          formData.append('chunk', i.toString())
          this.$Api
            ._postMultiData({
              url: this.$Api.apiModulesList.videoUpload.upload.url,
              method: 'post',
              headers: {
                token: Cookies.get('token'),
                'Content-Type': 'multipart/form-data; boundary=ABCD',
              },
              data: formData,
            })
            .then((res: any) => {
              if(res && res.status){
                this.successChunk++ 
              } else {
                this.$AntMessage.warning('上传异常')
              }
            })
        }
      }
      
    })
  }

  sendMerge(){
    this.successChunk = 0
    this.$ModuleApis.videoUpload
    .merge({
      data: {
        baseId: this.uploadData.baseId,
        chunks: this.chunkList.length,
        md5File: this.md5,
        name: this.filename
      },
    })
    .then((res: any) => {
      if (res.code === window.CROSS_CODE) {
        this.$AntMessage.success('上传成功')
        this.handleUploadSuccess()
      }
    })
  }

  /**
   * 计算md5,实现断点续传及秒传
   * @param file
   */
  computeMD5(file: any) {
    const _this = this
    this.filename = file.name
    return new Promise((resolve, reject)=>{
      try {
        let fileReader = new FileReader();
        let time = new Date().getTime();
        let blobSlice = File.prototype.slice
        let currentChunk = 0;
        const chunkSize = 5 * 1024 * 1024;
        let chunks = Math.ceil(file.size / chunkSize);
        let spark = new SparkMD5.ArrayBuffer();
        loadNext();
        fileReader.onload = ((e: any) => {
            spark.append(e.target.result);
            currentChunk++;
            if (currentChunk < chunks) {
                loadNext();
            } else {
                this.md5 = spark.end();
                resolve(this.md5)
                console.log(`MD5计算完毕:${file.name} \nMD5:${this.md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
            }
        });
        fileReader.onerror = function () {
            // this.error(`文件${file.name}读取出错,请检查该文件`)
            // file.cancel();
        };
        function loadNext() {
            let start = currentChunk * chunkSize;
            let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
            fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
            _this.chunkList.push(blobSlice.call(file, start, end))
        }
      }catch(e) {
        reject(e)
      }
    })
  }



  /**
   * @description 删除
   * @author YXM
   */
  handleRemove(file: any, fileList: any) {
    this.fileList.splice(this.fileList.indexOf(file), 1)
  }

  /**
   * @description  每次上传时,都会触发这个方法
   * @author YXM
   */
  handleChange(info: any) {
    this.$AntMessage.destroy()
    if (info.file.response) {
      this.$AntMessage.success('上传成功')
      this.handleUploadSuccess()
    }
  }

  /**
   * @description 拖拽文件不符合 accept 类型时的回调
   * @author YXM
   */
  handleReject(fileList: any) {
    this.$AntMessage.warning(`请选择 ${this.videoUploadData.acceptType} 格式的文件执行上传操作`)
  }



  /**
   * @description 上传成功之后告诉父组件
   * @author YXM
   */
  @Emit('emitUploadSuccess')
  handleUploadSuccess() {}

  /**
   * @description css样式代码
   * @author YXM
   */
  style: any = {
    box: {
      // 上传的盒子
      // width: '100%',
      // textAlign: 'center',
    },
    btu: {
      padding: '30px 35px 70px 35px',
    },
    icon: {
      fontSize: '40px',
    },
    modal: {
      // modal对话框样式
      modal: {
        maxWidth: '30%',
        maxHidth: '80%',
      },
      video: {
        width: '100%',
      },
    },
  }
}

注意:

1.分片的大小可能会对字节流上传有影响,表现为我设置分片大小为10M,上传了11M的文件,按分片分别得到10M、1M的字节流,上传过程中控制台显示接口并没有接收到10M的字节流,并无深究此问题,改成5M就能够上传了,因此在这里备注下;

2.有问题再补充吧。

参考文章(正文来了【滑稽】):

Spring Boot[五]:WebUploader分片断点上传

基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件

js文件分片上传

https://github.com/shady-xia/Blog/blob/master/vue-simple-uploader/globalUploader.vue

Antd Upload 文件加密及分片上传实现逻辑

Vue2.0结合webuploader实现文件分片上传

sparkmd5+FileReader实现文件分段上传,断点续传

asp.net大文件分块上传视频教程

猜你喜欢

转载自blog.csdn.net/rrrrroy_Ha/article/details/109685713