在Uniapp中实现大文件切片上传

组件功能

这个视频上传组件的功能包括:

  • 选择视频文件:通过点击"选择文件"按钮选择要上传的视频文件。
  • 显示上传进度:在上传过程中,展示上传进度条以及视频文件的名称、大小和时长信息。
  • 使用 simple-uploader.js 实现上传:利用 simple-uploader.js 这个库进行视频文件的上传。
  • 计算视频时长:通过使用浏览器的原生 API 获取上传视频文件的时长。
  • 计算文件 MD5 哈希值:使用 SparkMD5 库计算视频文件的 MD5 哈希值,用于文件的唯一标识和校验。

这个组件通过以下步骤实现:

  1. 选择文件:点击"选择文件"按钮,调用 choiceFileHandle 方法,通过 uni.chooseVideo 方法选择视频文件并添加到上传队列中。
  2. 上传进度展示:利用 u-slider 组件展示上传进度。
  3. 初始化上传器:在 mounted 钩子函数中初始化 simple-uploader.js 的实例,并设置相关参数。
  4. 文件上传事件监听:监听 simple-uploader.js 的文件上传事件,包括文件添加、上传进度和上传成功。
  5. 计算文件时长和 MD5:通过原生 API 和 SparkMD5 库计算文件时长和 MD5 哈希值。

1.使用依赖

simple-uploader.js

shell 复制代码
npm install simple-uploader.js

spark-md5

shell 复制代码
npm install --save spark-md5

2.uploader初始化

  • 创建一个 Uploader 实例
  • 实例化后添加监听事件
js 复制代码
this.uploader = new Uploader({
    // 单文件上传
    singleFile: true,
    headers: {
      Authorization: "Bearer " + getToken(),
    },
    //目标上传 URL,默认POST
    target: config.baseUrl + "/system/uploader/chunk",
    //分块大小(单位:字节)
    chunkSize: 1024 * 1024 * 2,
    //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
    fileParameterName: "upfile",
    //失败后最多自动重试上传次数
    maxChunkRetries: 3,
    //是否开启服务器分片校验,对应GET类型同名的target URL
    testChunks: true,
    /*
      服务器分片校验函数,判断秒传及断点续传,传入的参数是Uploader.Chunk实例以及请求响应信息
      reponse码是successStatuses码时,才会进入该方法
      reponse码如果返回的是permanentErrors 中的状态码,不会进入该方法,直接进入onFileError函数 ,并显示上传失败
      reponse码是其他状态码,不会进入该方法,正常走标准上传
      checkChunkUploadedByResponse函数直接return true的话,不再调用上传接口
    */
    checkChunkUploadedByResponse: function (chunk, response_msg) {
      let objMessage = JSON.parse(response_msg);
      if (objMessage.skipUpload) {
        return true;
      }
      return (
        (objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0
      );
    },
  })

  // 文件添加 单个文件
  this.uploader.on('fileAdded', (file, event)=>{
    this.computeMD5(file);
  })
  // 文件在上传中
  this.uploader.on('fileProgress', (rootFile, file, chunk) => {
    const res = JSON.parse(chunk.processedState?.res ?? '{}');
    // console.log(res,chunk);
    if (res==200 || !chunk.preprocessState) {
      this.uploadProgress = (this.uploader.progress()*100).toFixed(2);
    } else {
      this.cancel();
    }
  })
  // 文件上传成功
  this.uploader.on('fileSuccess', (rootFile, file, message) => {
    //refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等
    file.refProjectId = "123456789";
    // 文件合并接口
    mergeFile(file).then(res=>{
      if (res.code == 200) {
        this.$emit("updateValue", res.data.location);
      }
    })
  })

3.使用spark-md5文件切片

js 复制代码
computeMD5(file) {
  file.pause();
  console.log("文件大小:" + file.size);

  let fileReader = new FileReader();
  let time = new Date().getTime();
  let blobSlice =
    File.prototype.slice ||
    File.prototype.mozSlice ||
    File.prototype.webkitSlice;
  let currentChunk = 0;
  const chunkSize = 10 * 1024 * 1000;
  let chunks = Math.ceil(file.size / chunkSize);
  let spark = new SparkMD5.ArrayBuffer();
  //由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
  let chunkNumberMD5 = 1;

  loadNext();

  fileReader.onload = (e) => {
    spark.append(e.target.result);

    if (currentChunk < chunkNumberMD5) {
      loadNext();
    } else {
      let md5 = spark.end();
      file.uniqueIdentifier = md5;
      file.resume();
      console.log(
        `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
          file.size
        } 用时:${new Date().getTime() - time} ms`
      );
    }
  };

  fileReader.onerror = function () {
    console.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.file, start, end));
    currentChunk++;
    console.log("计算第" + currentChunk + "块");
  }
}

4.文件选择

我这里用的是uni.chooseVideo(OBJECT) 拍摄视频或从手机相册中选视频 this.uploader.addFile(file)中需传入File类型

js 复制代码
/**
 * 选择文件
 */
choiceFileHandle(){
  this.uploader && this.uploader.cancel();
  uni.chooseVideo({
    success: async (res)=>{
      let file = res.tempFile;
      this.uploader.addFile(file);
      this.showProgress = true;
      this.fileData.name = file.name;
      this.fileData.size = (file.size/1024/1024).toFixed(2) + 'MB';
      this.fileData.time = await this.videoLong(file);
      this.$emit("updateTime", this.fileData.time);
    }
  })
}

完整代码

js 复制代码
<template>
  <view class="upload">
    <view class="upload-box" v-if="showProgress">
      <view class="upload-box-top">
        <image
          src="@/static/images/app/mp4.png"
          mode="scaleToFill"
        />
        <view class="upload-box-top-center">
          <p class="name-text">{{ fileData.name }}</p>
          <p class="size-text">{{ fileData.size }}</p>
        </view>
      </view>
      <u-slider 
        v-model="uploadProgress" 
        activeColor="#3c9cff" 
        inactiveColor="#c0c4cc"
        disabled
        min="0"
        max="100"
      ></u-slider>
    </view>
    <u-button 
      text="选择文件" 
      color="#0366F1"
      size="small"
      style="width: 100px;float: left;"
      @click="choiceFileHandle"
    ></u-button>
  </view>
</template>

<script>
import Uploader from 'simple-uploader.js';
import SparkMD5 from "spark-md5";
import { getToken } from '@/utils/auth';
import config from '@/config'
import { mergeFile } from "@/api/promoteLearning.js"
export default {
  name: "uploadVideo",
  props: {
    value: {
      type: String,
      default: ''
    },
    time: {
      type: Number,
      default: ''
    }
  },
  data() {
    return {
      fileData: {
        name: "",
        size: "",
        time: ""
      },
      showProgress: false,
      uploadProgress: 0,
      uploader: null,
    };
  },
  mounted() {
    this.initUploader();
  },
  methods: {
    /**
     * 选择文件
     */
    choiceFileHandle(){
      this.cancel();
      uni.chooseVideo({
        success: async (res)=>{
          let file = res.tempFile;
          this.uploader.addFile(file);
          this.showProgress = true;
          this.fileData.name = file.name;
          this.fileData.size = (file.size/1024/1024).toFixed(2) + 'MB';
          this.fileData.time = await this.videoLong(file);
          this.$emit("updateTime", this.fileData.time);
        }
      })
    },
    cancel() {
      this.fileData = {
        name: "",
        size: ""
      };
      this.showProgress = false;
      this.uploadProgress = 0;
      this.uploader && this.uploader.cancel();
    },
    videoLong(file){
      return new Promise((resolve)=>{
        var url = URL.createObjectURL(file);
        var audioElement = new Audio(url);
        audioElement.addEventListener("loadedmetadata", function() {
          resolve(audioElement.duration)
        });
      })
    },
    initUploader() {
      this.uploader = new Uploader({
        // 单文件上传
        singleFile: true,
        headers: {
          Authorization: "Bearer " + getToken(),
        },
        //目标上传 URL,默认POST
        target: config.baseUrl + "/system/uploader/chunk",
        //分块大小(单位:字节)
        chunkSize: 1024 * 1024 * 2,
        //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
        fileParameterName: "upfile",
        //失败后最多自动重试上传次数
        maxChunkRetries: 3,
        //是否开启服务器分片校验,对应GET类型同名的target URL
        testChunks: true,
        /*
          服务器分片校验函数,判断秒传及断点续传,传入的参数是Uploader.Chunk实例以及请求响应信息
          reponse码是successStatuses码时,才会进入该方法
          reponse码如果返回的是permanentErrors 中的状态码,不会进入该方法,直接进入onFileError函数 ,并显示上传失败
          reponse码是其他状态码,不会进入该方法,正常走标准上传
          checkChunkUploadedByResponse函数直接return true的话,不再调用上传接口
        */
        checkChunkUploadedByResponse: function (chunk, response_msg) {
          let objMessage = JSON.parse(response_msg);
          if (objMessage.skipUpload) {
            return true;
          }
          return (
            (objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0
          );
        },
      })

      // 文件添加 单个文件
      this.uploader.on('fileAdded', (file, event)=>{
        this.computeMD5(file);
      })
      this.uploader.on('fileProgress', (rootFile, file, chunk) => {
        const res = JSON.parse(chunk.processedState?.res ?? '{}');
        // console.log(res,chunk);
        if (res==200 || !chunk.preprocessState) {
          this.uploadProgress = (this.uploader.progress()*100).toFixed(2);
        } else {
          this.cancel();
        }
      })
      this.uploader.on('fileSuccess', (rootFile, file, message) => {
        console.log('fileSuccess',rootFile, file, message);
        file.refProjectId = "123456789";
        mergeFile(file).then(res=>{
          if (res.code == 200) {
            this.$emit("updateValue", res.data.location);
          }
        })
      })
    },
    computeMD5(file) {
      file.pause();
      console.log("文件大小:" + file.size);

      let fileReader = new FileReader();
      let time = new Date().getTime();
      let blobSlice =
        File.prototype.slice ||
        File.prototype.mozSlice ||
        File.prototype.webkitSlice;
      let currentChunk = 0;
      const chunkSize = 10 * 1024 * 1000;
      let chunks = Math.ceil(file.size / chunkSize);
      let spark = new SparkMD5.ArrayBuffer();
      //由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
      let chunkNumberMD5 = 1;

      loadNext();

      fileReader.onload = (e) => {
        spark.append(e.target.result);

        if (currentChunk < chunkNumberMD5) {
          loadNext();
        } else {
          let md5 = spark.end();
          file.uniqueIdentifier = md5;
          file.resume();
          console.log(
            `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
              file.size
            } 用时:${new Date().getTime() - time} ms`
          );
        }
      };

      fileReader.onerror = function () {
        console.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.file, start, end));
        currentChunk++;
        console.log("计算第" + currentChunk + "块");
      }
    }
  },
};
</script>

<style lang="scss" scoped>
.upload {
  width: 100%;
}
.upload-box {
  width: 100%;
  padding: 10px;
  border-radius: 4px;
  border: 1px solid #e4e7ed;
  box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
  margin-bottom: 10px;
  &-top {
    display: flex;
    image {
      width: 40px;
      height: 40px;
      margin-right: 10px;
    }
    &-center {
      display: flex;
      flex-direction: column;
      justify-content: space-around;
      .name-text {
        font-size: 14px;
        color: #4F4F4F;
        font-weight: 700;
      }
      .size-text {
        font-size: 13px;
        color: #828282;
      }
    }
  }
}
::v-deep {
  .uni-slider-thumb {
    display: none;
  }
  uni-slider {
    margin: 10px 0 0;
  }
}
</style>
相关推荐
花木偶3 分钟前
【郑大二年级信安小学期】Day9:XSS跨站攻击&XSS绕过&CSRF漏洞&SSRF漏洞
前端·xss
FogLetter4 分钟前
节流(Throttle):给频繁触发的事件装上"冷却时间"
前端·javascript
小公主6 分钟前
彻底搞懂 Event Loop!这篇文章帮你一次性吃透宏任务、微任务、执行顺序
前端·javascript
xiaominlaopodaren10 分钟前
爱心动画的数学之美:从心形曲线到粒子系统
前端
AI悦创Python辅导26 分钟前
如何挑选适合项目场景的数据分析工具?
前端
用户92724725021929 分钟前
新闻自动采集并通过API发布到博客
前端·后端
清风920033 分钟前
Logback——日志技术(基础)
java·前端·logback
EndingCoder33 分钟前
排序算法与前端交互优化
开发语言·前端·javascript·算法·排序算法·交互
三月的一天1 小时前
在 React Three Fiber 中实现 3D 模型点击扩散波效果
前端·react.js·前端框架
爱敲代码的小冰1 小时前
npm 切换 node 版本 和npm的源
前端·npm·node.js