基于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大文件分块上传视频教程

<think>好的,用户之前已经得到了一个带有自定义快进快退按钮的HTML5视频播放器代码,现在他们想要添加可拖动的进度条。我需要先理解用户的具体需求,然后思考如何实现这个功能。 首先,用户可能希望有一个进度条,可以显示视频的当前播放进度,并且允许通过拖动来调整播放位置。这通常包括两个部分:显示进度的条和一个可以拖动的滑块。此外,还需要处理用户的交互事件,比如点击进度条跳转到相应位置,或者拖动滑块调整时间。 接下来,我需要考虑如何将这些元素添加到现有的代码中。现有的HTML结构里有一个.controls的div,里面已经包含了快退、播放/暂停和快进按钮。我应该在这个div中添加进度条的相关元素,比如一个进度条容器,里面包含背景条、缓冲条、当前进度条和滑块。 然后,考虑CSS样式。需要设置进度条容器的背景颜色,当前进度的颜色,缓冲进度的颜色,以及滑块的外观。要注意层级关系,确保当前进度条在缓冲条之上,滑块在最上层,并且可以拖动。 接下来是JavaScript部分。需要实现以下几个功能: 1. 实时更新进度条的位置,反映当前播放时间。 2. 允许用户点击进度条的不同位置来跳转到对应的时间。 3. 允许用户拖动滑块来调整播放时间。 4. 显示视频的缓冲进度。 首先,在JavaScript中获取新添加的DOM元素,比如progressContainer、progressBar、bufferBar、slider。然后,监听视频的timeupdate事件,在回调中更新进度条的位置。同时,监听loadedmetadata事件来获取视频的总时长,用于计算进度百分比。 对于点击进度条跳转的功能,需要给progressContainer添加点击事件监听器,计算点击位置相对于容器的百分比,然后设置视频的currentTime。 拖动滑块的功能稍微复杂一些。需要处理mousedown、mousemove和mouseup事件。当用户按下滑块时,开始拖动,记录初始位置,并在拖动过程中计算偏移量,更新进度条和视频的当前时间。鼠标松开时,移除事件监听器。 另外,缓冲进度条需要监听视频的progress事件,根据已缓冲的时间段来更新bufferBar的宽度。 还需要考虑用户体验,比如在拖动滑块时,视频应该暂停,拖动结束后再根据之前的状态决定是否继续播放。此外,进度条和滑块的样式需要美观,并且与现有的控制条风格一致。 在实现过程中,可能会遇到一些问题,比如进度条更新不流畅,或者拖动时出现卡顿。需要确保事件监听和更新操作高效,避免过多的重绘。此外,跨浏览器的兼容性也需要考虑,比如touch事件对于移动设备的支持,但用户当前的需求可能暂时只需桌面端的支持,所以可以先实现鼠标事件。 最后,需要测试所有功能,确保进度条正确显示和响应操作,拖动平滑,时间跳转准确,缓冲进度正确更新。同时,检查键盘快捷键是否仍然有效,不会与新功能冲突。 总结,添加可拖动的进度条需要修改HTML结构,添加CSS样式,并通过JavaScript处理多个事件和状态更新,确保与现有功能的协调工作。</think>以下是在原有代码基础上增加 **可拖动进度条** 的完整修改版代码: ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>带进度条的视频播放器</title> <style> /* 新增进度条样式 */ .progress-container { height: 10px; background: #666; border-radius: 5px; margin: 10px 0; cursor: pointer; position: relative; } #progressBar { height: 100%; background: #ff4444; border-radius: 5px; width: 0; position: relative; } #bufferBar { height: 100%; background: #888; border-radius: 5px; width: 0; position: absolute; top: 0; left: 0; z-index: -1; } #slider { width: 15px; height: 15px; background: #fff; border-radius: 50%; position: absolute; top: -2px; left: 0; cursor: pointer; } </style> </head> <body> <!-- 修改后的控制条 --> <div class="controls"> <!-- 新增进度条 --> <div class="progress-container" id="progressContainer"> <div id="bufferBar"></div> <div id="progressBar"> <div id="slider"></div> </div> </div> <!-- 原有按钮 --> <button onclick="skip(-10)">快退10秒</button> <button onclick="playPause()">播放/暂停</button> <button onclick="skip(10)">快进10秒</button> </div> <script> // 新增DOM元素获取 const progressBar = document.getElementById('progressBar'); const progressContainer = document.getElementById('progressContainer'); const slider = document.getElementById('slider'); const bufferBar = document.getElementById('bufferBar'); // 进度条拖动功能 let isDragging = false; // 鼠标按下开始拖动 slider.addEventListener('mousedown', () => { isDragging = true; video.pause(); }); // 鼠标移动更新进度 document.addEventListener('mousemove', (e) => { if (isDragging) { const rect = progressContainer.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; const time = pos * video.duration; video.currentTime = time > video.duration ? video.duration : time; } }); // 鼠标松开结束拖动 document.addEventListener('mouseup', () => { isDragging = false; if (!video.paused) video.play(); }); // 点击进度条跳转 progressContainer.addEventListener('click', (e) => { const rect = progressContainer.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; video.currentTime = pos * video.duration; }); // 视频时间更新时同步进度条 video.addEventListener('timeupdate', () => { if (!isDragging) { const progress = (video.currentTime / video.duration) * 100; progressBar.style.width = progress + '%'; slider.style.left = `calc(${progress}% - 7px)`; } }); // 显示缓冲进度 video.addEventListener('progress', () => { if (video.buffered.length > 0) { const bufferProgress = (video.buffered.end(0) / video.duration) * 100; bufferBar.style.width = bufferProgress + '%'; } }); // 初始化视频时长 video.addEventListener('loadedmetadata', () => { progressBar.style.width = '0%'; }); </script> </body> </html> ``` --- ### 新增功能说明: 1. **双进度显示** - 红色进度条:当前播放进度 - 灰色缓冲条:视频预加载进度 2. **交互功能** - 拖动时自动暂停视频(释放后恢复) 3. **技术实现** - 通过 `getBoundingClientRect()` 获取元素位置 - 使用 `timeupdate` 事件实时同步进度 - 通过 `progress` 事件监测缓冲进度 - 使用 CSS `calc()` 实现滑块精准定位 --- ### 使用注意事项: 1. 进度条容器使用 `position: relative` 实现精确定位 2. 缓冲条使用 `z-index: -1` 确保在进度条下方显示 3. 拖动时通过 `isDragging` 标志位避免进度冲突 4. 通过 `loadedmetadata` 事件初始化进度条 如果需要增加时间显示(如当前时间/总时长),可以继续补充需求!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值