基于iview上传组件的分片上传

文章描述了一种处理大文件上传的策略,通过将文件按1M大小切分成多个二进制数据流,逐个上传并缓存上传进度。在上传过程中,如果遇到网络错误,可以从上次失败的位置继续。后端提供上传和合并接口,分别处理单个数据块接收和文件合并。前端使用Vue组件处理文件切片、转换及上传,并有状态管理来跟踪上传进度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

整体思路:

获取上传的文件,将文件按size分割成1M的二进制数据流数组,然后逐条创建为单个文件进行上传。等待全部上传后,调用合并接口,在后端进行对文件的合并处理。

由于是逐条单个文件上传的,所以如果过程中发生网络错误,会在浏览器缓存中记录上传的文件的在数组中的索引,下次继续上传时将从索引处开始上传。

后端提供2个接口:1是接受上传数据的接口。2是处理文件的合并接口。

前端部分:


<template> 
          <Upload
              :show-upload-list="false"
              :on-success="upVideoPicture"
              :before-upload="handeleBeforeUpVideo"
              :action="''"
              :data="contLaunchUrl"
              name="file"
              type="drag"
              style="display: inline-block; width: 150px"  >

            <div style="width: 150px; height: 105px; line-height: 105px">
              <Icon type="ios-camera" size="20"></Icon>
            </div> 

            <Spin fix v-if="uploadVideoLoading">
                <Icon type="ios-loading" size=18 class="demo-spin-icon-load"></Icon>
                <div>Loading</div>
            </Spin>

          </Upload>
 
</template>
<script> 
export default {
  data(){
    return {
        contLaunchUrl:{},
        uploadVideoLoading:false,
    }
  },
  methods:{ 
      upVideoPicture(data){
        console.log("ok",data)
      },
      handeleBeforeUpVideo(file){
        this.uploadVideoLoading = true; 
        this.fileChunk(file)
        return false;//阻止默认行为
      }, 

      // 文件切片
      fileChunk(file){
        let that = this
        // 文件名称设置为时间戳+四位随机数
        let name = new Date().getTime() + '' + (Math.floor(Math.random()*(9999-1000))+1000);
        let fileName = name  // 获取文件名
        let fileType = `${file.name.split('.')[1]}` // 文件类型后缀
        let chunkSize = 1 * 1024 * 1024   // 示例1M
        let chunkList = []  // 创建一个数组用来存储每一片文件流数据
        if (file.size < chunkSize) {  // 如果文件大小小于1M就只有一片,不用切
          chunkList.push(file.slice(0))  // 文件流从第一个字节直接截取到最后就可以了
        } else {  // 如果文件大小大于1M 就需要切片了
          var start = 0, end = 0  // 创建两个变量 开始位置 结束位置
          while (true) {  // 循环
            end += chunkSize  // 结束为止 = 结束位置 + 每片大小
            let blob = file.slice(start, end)  // 文件流从开始位置截取到结束位置
            start += chunkSize  // 截取完,开始位置后移
            if (!blob.size) {  // 如果截取不到了就退出  检查截取的情况
              break;
            }
            chunkList.push(blob)  // 把截取的每一片数据保存到数组
          }
        } 
        //一共分了多少片
        that.transformFileType(chunkList,fileName,fileType); // 文件类型转换
      },
      // 文件类型转换
      async transformFileType(list,name,type){ // list,name,type是上传时需要的参数,可根据情况而定
        let that = this
        let uploadFileNum = 0; // 已经上传的文件数量
        let num = localStorage.getItem("uploadFileNum")?localStorage.getItem("uploadFileNum"):0 // 已经上传文件数量
        // 如果已经全部上传 直接合并
        if(num == list.length) return that.mergeFileFun(name,list.length,type)
        //刚开始num为0
        for (var i = num; i < list.length; i++) {
          // 这么写是因为文件转换是异步任务
          let transToFile = async (blob, fileName, fileType) => {
            return new window.File([blob], fileName, {type: fileType})
          }
          // 转换完成后可以将file对象传给接口 fileFormData 是需要传递的参数
          let fileFormData = new FormData();
          const transToFileRes = await transToFile(list[i], "video", type)
          fileFormData.append('file',transToFileRes) //文件内容
          fileFormData.append('fileName',name) //文件名称
          fileFormData.append('fileNum',Number(i)+1) //文件次数
          fileFormData.append('filePath','video') //文件夹地址
          await that.uploadFileMethods(fileFormData,i,name,list,type) // 上传文件
        }
      },
      // 上传文件
      async uploadFileMethods(fileFormData,uploadFileNum,name,list,type) {
        let that = this 
        //上传
        this.$API.target.base.uploadFile(fileFormData).then(res => { 
          if(res.code != 200){
            that.uploadVideoLoading = false;
            that.$Message.error(res.message);
          }
          uploadFileNum = Number(uploadFileNum) + 1
          localStorage.setItem("uploadFileNum", uploadFileNum); // 已经上传文件数量
          // 如果已经上传数量uploadFileNum与切片长度一致,就开始合并文件
          if(uploadFileNum == list.length){
            that.mergeFileFun(name,list.length,type);
          }
        })
      },
      // 合并文件
      mergeFileFun(name,fileNo,type){ 
        let that = this
        let fileFormData = new FormData(); // 合并文件接口参数
        fileFormData.append('fileName',name) // 合并的该套文件的文件名
        fileFormData.append('fileTotalNum',fileNo) // 该套文件总数量
        fileFormData.append('fileSuffix',type) // 文件类型后缀
        fileFormData.append('filePath','video') // 该套文件存放一级路径

        
        this.$API.target.base.mergeFile(fileFormData).then(res => {
          if(res.code != 200){
            that.uploadVideoLoading = false;
            that.$Message.error(res.message);
          }
          that.uploadVideoLoading = false;
          
          that.$Message.info("文件上传至:"+res.data); // 后端返回的文件路径
          localStorage.removeItem('uploadFileNum') // 合并成功后清除已经上传文件数量标记
        })
      },




  }
}
</script>
<style scoped>

</style>

请求接口封装:

 

//上传接口
export function uploadFile(data){ 
    return productInstance({
        url: "/protal/uploadfile",
        method: "post", 
        data:data,
    }).then(res => res.data)
}
//合并接口
export function mergeFile(data){
    return productInstance({
        url: "/protal/mergefile",
        method: "post", 
        data:data,
    }).then(res => res.data)
}

后端部分:


    //大文件上传接口
    @PostMapping("/uploadfile")
    public Response<Boolean> uploadfile(@RequestParam(value = "file") MultipartFile file,
                                       @RequestParam(value = "fileName") String fileName,
                                       @RequestParam(value = "fileNum") int fileNum,
                                       @RequestParam(value = "filePath") String filePath){
        try{
            //只接受文件的过程
            File f = new File("D:\\Temp\\"+filePath+"\\"+ fileName );
            if(!f.exists()){ f.mkdirs(); }

            File newfile = new File("D:\\Temp\\"+filePath+"\\"+fileName+"\\"+ fileNum);
            file.transferTo(newfile);//如果没有就创建

 
            return new Response<>(ErrorCode.group0.getCode(),ErrorCode.group0.getMessage(),true);
        } catch (Exception ex){
            log.error("JqAccountTableController-------------login-------------ex:" + ex);
            return new Response<>(ErrorCode.group2.getCode(),ErrorCode.group2.getMessage());
        }
    }

    //大文件合并接口
    @PostMapping("/mergefile")
    public Response<String> mergefile(@RequestParam(value = "fileName") String fileName,
                                      @RequestParam(value = "fileTotalNum") String fileTotalNum,
                                      @RequestParam(value = "fileSuffix") String fileSuffix,
                                      @RequestParam(value = "filePath") String filePath){
        try{

            String from = "D:\\Temp\\"+filePath+"\\"+fileName;//数据源
            String to =   "D:\\Temp\\"+filePath+"\\"+ fileName + "." + fileSuffix;//最终生成的文件

            //新文件名
            File t = new File(to);
            FileOutputStream out = new FileOutputStream(t, true);
            FileChannel outChannel = out.getChannel();

            File fromFile = new File(from);
            // 获取目录下的每一个文件名,再将每个文件一次写入目标文件
            if (fromFile.exists()) {
                //从目录下获取全部的文件列表
                File dirFile = new File(from);
                File[] listFiles = dirFile.listFiles();
                List<File> list = Arrays.asList(listFiles);
                Collections.sort(list, (o1, o2) -> { return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName()); });

                FileInputStream in = null;
                FileChannel inChannel = null;
                long start = 0;// 记录新文件最后一个数据的位置
                for (File file : list) {
                    in = new FileInputStream(file);
                    inChannel = in.getChannel();
                    //将分片内容吸入到新文件中
                    outChannel.transferFrom(inChannel, start, file.length());
                    start += file.length();
                    in.close();//吸口关闭
                    inChannel.close();//吸口关闭
                }
            }

            //文件关闭
            out.close();
            outChannel.close();


            //删除文件夹以及数据
            if (fromFile.exists()) {
                //获取目录下子文件
                File[] files = fromFile.listFiles();
                //遍历该目录下的文件对象
                for (File f : files) {
                    //文件删除
                    f.delete();
                }
                //文件夹删除
                fromFile.delete();
            }

            //返回
            return new Response<>(ErrorCode.group0.getCode(),ErrorCode.group0.getMessage(),to);
        } catch (Exception ex){
            log.error("JqAccountTableController-------------login-------------ex:" + ex);
            return new Response<>(ErrorCode.group2.getCode(),ErrorCode.group2.getMessage());
        }
    } 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值