当产品说要批量上传8W个文件的时候

当产品说要批量上传8W个文件的时候

背景

最近一星期需求终于缓了下来,所以就准备对之前做的需求进行优化。在之前版本需求中,测试发现在批量上传的时候,数目一旦过多,并发请求太多导致浏览器崩溃。更可恶的是,产品已经准备好8W张图片准备一下子上传了,(我TM刀呢?)这样的话我这个小组件必然是撑不住这么多的并发的,也是无奈必须要对其进行优化。

现状分析

1.此时上传组件是直接使用element-uiupload组件进行开发的;

2.根据业务需要,文件需要上传到OSS服务器上,因此在上传前需要

  • 请求接口读取上传的配置(oss的上传地址以及一些上传参数);

  • 请求接口去给每一个资源创建目录;

  • 执行上传请求;

  • 上传完成后再次请求后端接口将文件入库。

3.这样一算起来,每一个资源在上传的时候,都会有4个请求的并发,在element这个组件没有队列的情况下,批量上传的时候会崩溃可想而知。

优化分析

每一个资源上传都需要请求4个接口,并且在批量选中的时候这些请求都会并发执行,这就导致了请求崩溃的主要原因。以下是我能想到的优化点:

  1. 每次上传的请求是否可以减少,考虑到执行上传请求和入库接口都不能被省略,只能考虑减少前两步请求;
    • oss的配置只在 created生命周期中请求一次,并不需要每次选择文件都去读取一次配置。
    • 第二步和后端交流中了解到,资源的目录创建就是接口返回固定的路径名称+生成一个的UUID作为资源在oss上的目录,那将生成UUID放在前端也就不用去请求接口让后端生成了,我们只要保证上传到oss的UUID和入库时候相同就可以了。
  2. 上传文件能否是一个队列,在上一个文件上传完成后再执行下一个文件的上传;
  3. 记录添加的文件数量、成功上传数量和失败数量,方便上传结束后再去刷新列表;
  4. 8W个文件形成的列表,采用虚拟dom,避免页面dom元素过多引起页面卡顿。

具体实现

准备工作

  • 考虑到需要队列上传,作者就采用了之前使用过的vue-simple-uploader这个上传轮子进行开发,这个插件还可以支持分片上传、秒传、断点续传等上传功能,非常的强大。vue-simple-uploader中有 simultaneousUploads的配置字段,表示默认同时上传的个数。
  • UUID的生成就使用前端库uuid来生成。
  • 虚拟dom作者采用vue-virtual-scroll-list来实现。

代码实现

html结构
<uploader
    v-if="isPageReady"
    :options="options"
    class="uploader-example"
    :autoStart="false"
    multiple
    ref="uploadVue"
    @file-added="onFileAdded"
    @file-success="onFileSuccess"
    @file-progress="onFileProgress"
    @file-error="onFileError"
  >
    <uploader-unsupport></uploader-unsupport>
    <uploader-btn ref="uploadBtn"></uploader-btn>
    <uploader-list v-show="fileListNumber && showUploadFile">
      <template slot-scope="props">
        <div class="title flex-vertical-center">
          <div v-if="fileListNumber !== successNumber + failNumber">
            <i
              class="el-icon-upload"
              style="font-size: 20px; color: #1890ff"
            ></i>
            <span style="margin-left: 20px"
              >上传中 {{ successNumber }}/{{ fileListNumber }}</span
            >
          </div>
          <div v-else>
            <span class="flex-vertical-center">
              <i style="font-size: 16px" class="iconfont iconwancheng"></i>
              <span style="margin-left: 10px">全部上传成功</span>
            </span>
          </div>

          <i
            v-if="!showFileList"
            @click="changeShowFileList"
            class="el-icon-arrow-down"
          ></i>
          <i v-else @click="changeShowFileList" class="el-icon-arrow-up"></i>
          <i @click="showUploadFile = false" class="iconfont iconguanbi"></i>
        </div>
        <div class="list" v-show="showFileList">
          <div v-for="file in props.fileList" :key="file.id">
            <uploader-file :file="file" :list="true">
              <template slot-scope="props">
                <div class="file-list">
                  <div class="file-img">
                    <img :src="file.name | dealImg" />
                  </div>
                  <div class="item">
                    <div class="name">{{ file.name }}</div>
                    <div class="size">{{ file.size | dealFileSize }}</div>
                  </div>
                  <el-button
                    @click="linkToDetail(file)"
                    v-if="file.flag"
                    type="text"
                    style="margin-right: 10px"
                    >查看</el-button
                  >

                  <el-progress
                    v-if="props.progress * 100 != 100"
                    type="circle"
                    :width="16"
                    :stroke-width="1"
                    :show-text="false"
                    :percentage="parseInt(props.progress * 100)"
                  ></el-progress>
                  <i v-else class="iconfont iconwancheng"></i>
                </div>
              </template>
            </uploader-file>
          </div>
        </div>
      </template>
    </uploader-list>
  </uploader>

注意v-if=“isPageReady”,上文提到我们需要在created生命周期中获取上传的配置,所以在请求完配置接口后再进行实例化组件

import { v4 as uuidv4 } from "uuid";
  data() {
    return {
      isPageReady: false,
      FileLimit: {
        FileMaxSize: 1, // 允许上传的大小 单位(G)
        FileUnit: "G", // 单位
      },
      showUploadFile: true,
      fileListNumber: 0,
      successNumber: 0,
      failNumber: 0,
      listArr: [],
    };
  },
created() {
 Promise.all([this.getOssUploadParameter()]).then((res) => {
      this.isPageReady = true;
      this.$nextTick(() => {
      //vue-simple-upload内自己生成唯一值的方法会有重复,所以将uuid作为file的唯一值
        this.$refs.uploadVue.uploader.opts.generateUniqueIdentifier = () => {
          return uuidv4();
        };
        //vue-simple-upload 上传动态参数设置,如果是固定参数直接在query里面定义
         this.$refs.uploadVue.uploader.opts.processParams = (par) => {
            let obj = this.listArr.find(
              (item) => item.identifier === par.identifier
            );
            //自定义上传参数
            par.name = obj.name;
            par.key = obj.key;
            return par;
          };
      });
    });
},
methods:{
   // 文件添加事件
    onFileAdded(file) {
    //业务需要,显示上传的文件列表
      if (!this.showUploadFile) {
        this.showUploadFile = true;
      }
      file.pause();
      if (file.isFolder) {
        this.$message.error("不支持文件夹上传");
        return false;
      }
      // 文件大小判断
      const isSize = this.checkSize(file.size);
      if (!isSize) {
        file.ignored = true; // 过滤文件
        file.cancel(); // 停止上传
        return false;
      }
      this.fileListNumber++;
        this.listArr.push({
          identifier: file.uniqueIdentifier,//此时这里的uniqueIdentifier 就是我们自己生成的uuid
          name: encodeURI(file.name),//防止+等特殊字符上传oss会有编码问题,所以编码一下
          key: decodeURIComponent(
            this.key + "/" + file.uniqueIdentifier + "/${filename}"
          ),
        });

      this.$nextTick(() => {
        file.resume();
      });
    },
    //获取上传配置
     getOssUploadParameter() {
      let that = this;
      return new Promise(function (resolve, reject) {
        that
          .$get("upload/GetOssUploadParameter")
          .then((res) => {
            if (res && res.code == "0") {
               that.options = {
                  query: {
                    OSSAccessKeyId: res.data.OSSAccessKeyId,
                    policy: res.data.policy,
                    signature: res.data.signature,
                    success_action_status: 200,
                  },
                  target: res.data.uploadUrl,
                  fileParameterName: "file", //上传文件时文件的参数名,默认file
                  chunkSize: 1 * 1024 * 1024 * 1024, //1G一个分片
                  testChunks: false, //是否测试每个块是否在服务端已经上传了,主要用来实现秒传、跨浏览器上传等,默认 true
                  simultaneousUploads: 1, //默认同时上传的个数
                  allowDuplicateUploads: true, //如果说一个文件以及上传过了是否还允许再次上传。默认的话如果已经上传了,除非你移除了否则是不会再次重新上传的
                };
                resolve(true);
              }
            }
          })
          .catch((err) => {
            reject();
            console.log(err);
          });
      });
    },
    //上传错误
    onFileError(rootFile, file, response, chunk) {
      this.failNumber++;
      //   this.failFileList.push(rootFile);
    },
    // 检查文件大小
    checkSize(fileSize) {
      var FileLimit = this.FileLimit;
      var isSize = fileSize / 1024 / 1024 / 1024 < FileLimit.FileMaxSize; // 单位(M)
      if (!isSize) {
        this.$message.warning(
          " 文件大小不能大于" + FileLimit.FileMaxSize + FileLimit.FileUnit + "!"
        );
        return false;
      }
      return true;
    },
    
      //文件上传成功之后的回调
    onFileSuccess(rootFile, file, response, chunk) {
    
    let params = {
          name: file.name,
          resId: file.uniqueIdentifier,
          fileSize: file.size,
        };
        //调用后台入库接口,将上传的文件入库
        this.$post("upload/AddRes", params)
          .then((res) => {
            if (res.code == 0) {
              this.successNumber++;
              this.$set(file, "flag", true);
              
              if (this.successNumber + this.failNumber == this.fileListNumber) {
              //全部上传完成通知父组件事件
                this.$emit("uploadEnd", {
                  data: res.data,
                  flag: true,
                });
              } else {
               //当前文件上传完成通知父组件事件
                this.$emit("uploadEnd", {
                  data: res.data,
                  flag: false,
                });
              }
            } else {
              this.$message.error(res.msg);
            }
          })
          .catch((err) => {
            console.log(err);
          });
    },
}

最终实现效果

在这里插入图片描述

大家可以看到请求终于不是2000个并发了,而是一个一个队列上传,至此,只要服务器不挂,8W个文件也可以批量上传。

最后,大家有什么不明白的可以在下方评论区留言,喜欢的小伙伴记得给作者点个赞哦ღ( ´・ᴗ・` )比心~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值