[可乐的随手记 - 6] 并发控制上传大量的文件 (worker + yield )

前言

个人中后期加入项目 单机离线项 B端 前端 vue2全家桶 后端 Spring + mybatis-plus

本文仅作为自己的记录, 代码非原项目代码, 本文仅作为个人记录

业务场景以及部分截图

前端上传大量的数据表 (未限制文件个数), 后端做数据解析, 提取, 治理, 公司电脑配置: 6700U + 16G内存, Chrome 浏览器

文件上传

原生的Input元素作为上传的载体并隐藏元素

html 复制代码
<input
    multiple
    type="file"
    :accept="accept"
    ref="uploadFileInputElement"
    @change="handleInputValueChange"
/>

<el-button @click="handleClickUploadFile(false)">
    上传文件
</el-button>

<el-button @click="handleClickUploadFile(true)" >
  上传文件夹
</el-button>  

点击触发上传文件或者上传文件夹, 以及回调处理

js 复制代码
handleClickUploadFile(folder = false) {
    const fileInputElement = this.$refs.uploadFileInputElement;
    if (!fileInputElement) return;
    triggerUploadFile(fileInputElement, folder);
    folder ? uploadFolder : uploadFile
}

// 移除上传文件夹的实验性属性
const uploadFile = (fileInputElement) => {
    fileInputElement.removeAttribute('webkitdirectory')
}
// 增加上传文件夹的实验性属性
const uploadFolder = (fileInputElement) => {
    fileInputElement.setAttribute('webkitdirectory', true)
}

handleInputValueChange(event) {
  const { files } = event.target;
  const fileList = [...files];
  if (fileList.length) {
    // 显示准备上传的对话框, 界面上先给的一些反馈
    this.$refs.uploadFileProgressRef?.showReadyStatus();
  }
  useQueueUploadFile(fileList, {
    // 子线程过滤文件的回调
    filterFile: ({ index, filterCount, fileTotal }) => {
      this.readyStatusTips = `正在处理第${index}个文件, 共${fileTotal}个文件, ${filterCount}个文件被过滤, 请稍等...`;
    },
    uploadBegin: (needTime) => {
      // 前端假上传的所需时间
      this.totalTime = needTime;
      // 开启假上传进度条
      this.$nextTick(() => {
        this.$refs.uploadFileProgressRef?.startProgress();
      });
    },
    // 上传完成清空input元素的value
    uploadEnd: () => {
      if (this.$refs.uploadFileInputElement) {
        this.$refs.uploadFileInputElement.value = "";
      }
    },
    uploadSuccess: (val) => {
      this.finishCount = val;
      this.$nextTick(() => {
        // 上传完成, 进度条直接完成推进成100%的进度
        this.$refs.uploadFileProgressRef?.doneProgress();
      });
    },
    uploadFail: () => {
      this.finishCount = 0;
      this.$refs.uploadFileProgressRef?.stopProgress();
    },
  });
}

上传控制函数

js 复制代码
// useQueueUploadFile.js
const useQueueUploadFile = async (files, options) => {
  const { uid, filterFile, uploadBegin, uploadEnd, uploadSuccess, uploadFail } = options

  const limit = 6

  let uploadFileCount = 0

  const acceptFiles = ["xls", "xlsx", "csv", "zip", "rar"]

  const awaitUploadFiles = []

  const childrenThreadFilterFileTask = () => {
    return new Promise((resolve) => {
      const { createWorker, destroyWorker } = useWorker()
      const filterWorker = createWorker(useFilterFileInWorkerRoom)
      const fileSizeLimit = 500

      // 触发子线程的任务, 传入文件列表, 过滤的文件类型列表, 单个文件大小上限, 代码就不贴了
      filterWorker.postMessage({
        active: "filterFile",
        params: {
          files,
          acceptFiles,
          fileSizeLimit,
        }
      })

      // 子线程过滤回调
      filterWorker.onmessage = ({ data }) => {
        const { type, result } = data
        switch (type) {
          // 单个文件过滤失败并提示在界面上
          case "filterFileFail":
            const {
              fileName,
              fileSize,
              filterType,
            } = result
            const message = filterType ? `文件: ${fileName}不符合上传类型, 已被过滤上传` : `文件: ${fileName}体积过大(${fileSize}MB), 超过${fileSizeLimit}MB, 已被过滤上传`
            window.requestIdleCallback(() => Message.warning({ message, offset: 320 }))
            break;
          case "filterFileRuning":
            // 过滤正常显示个数同步给主线程进行展示
            filterFile?.(result)
            break;
          case "filterFileSuccess":
            // 文件过滤完成后将文件推入待上传数组并销毁子线程
            awaitUploadFiles.push(...result)
            destroyWorker()
            resolve()
            break;
          default:
            break;
        }
      }
    })
  }

  const mainThreadFilterFileTask = async () => {
    const offset = 320
    return new Promise((resolve) => {
      let tempFile = files[filterFileIndex]
      while (tempFile) {
        const { fileName } = getFileNameAndType(tempFile)
        if (useVerifyFileType(fileName, acceptFiles, { offset }) && !useVerifyFileSize(file, { limit: 500, offset })) {
          awaitUploadFiles.push(tempFile)
        } else {
          filterFileCount++
        }
        filterFile?.({
          index: filterFileIndex + 1,
          filterCount: filterFileCount,
          fileTotal: files.length,
        })
        filterFileIndex++
        tempFile = files[filterFileIndex] || null
        if (!tempFile) {
          resolve()
        }
      }
    })
  }

  window.Worker ? await childrenThreadFilterFileTask() : await mainThreadFilterFileTask()

  const uploadFileTaskEnd = () => {
    uploadEnd?.()
    uploadSuccessCount ? uploadSuccess?.(uploadSuccessCount) : uploadFail?.()
  }

  const awaitUploadFileCount = awaitUploadFiles.length

  if (awaitUploadFileCount === 0) {
    return uploadFileTaskEnd()
  }

  // 生成迭代器
  const iterator = (function* gen() {
    yield* awaitUploadFiles;
  })()

  // 队列上传任务
  const uploadFileTask = async () => {
    const { done, value: file } = iterator.next()

    if (uploadFileCount >= awaitUploadFileCount) {
      return uploadFileTaskEnd()
    }

    if (done) {
      return
    }

    const { fileName, fileType } = getFileNameAndType(file)

    try {
      const formData = new FormData();
      // 复制重新生成文件, 去除在选择文件夹模式下会存在文件夹的路径前缀
      const uploadFile = await copyFile(file, fileName)
      formData.append("file", uploadFile);
      const isZipFile = /(zip|rar)/gi.test(fileType)
      const uploadFunc = isZipFile ? uploadManyFileApi : uploadSingleFileApi ;
      const result = await uploadFunc(formData, uid);

      if (result.code === 200) {
        uploadSuccessCount++
      }
    } catch {
      window.requestIdleCallback(() => {
        Message.error({
          message: `文件上传出现异常, 文件: ${fileName}上传失败`,
          offset: 320,
        });
      })
    } finally {
      uploadFileCount++
    }
    return uploadFileTask()
  }

  // 计算总文件个数所需要的大概时间给假上传进度条
  const needTime = Math.max((awaitUploadFileCount / limit) * uploadFileNeedTime, uploadFileNeedTime)

  uploadBegin?.(needTime)

  // 控制并发数量列为6
  for (let index = 0; index < limit; index++) {
    uploadFileTask()
  }
}
相关推荐
Mintopia5 分钟前
图形学中的数学基础与 JavaScript 实践
前端·javascript·计算机图形学
Mintopia12 分钟前
Three.js 制作飘摇的草:从基础到进阶的全流程教学
前端·javascript·three.js
BillKu12 分钟前
Vue3父子组件数据双向同步实现方法
前端·javascript·vue.js
红尘散仙32 分钟前
七、WebGPU 基础入门——Texture 纹理
前端·rust·gpu
jaywongX33 分钟前
Base64编码原理:二进制数据与文本的转换技术
前端·javascript·vue
红尘散仙33 分钟前
八、WebGPU 基础入门——加载图像纹理
前端·rust·gpu
佳腾_36 分钟前
【Web应用服务器_Tomcat】一、Tomcat基础与核心功能详解
java·前端·中间件·tomcat·web应用服务器
天天扭码1 小时前
深入讲解Javascript中的常用数组操作函数
前端·javascript·面试
猿究院_xyz1 小时前
跟着尚硅谷学vue-day5
前端·javascript·vue.js·前端框架·html
小杰love编程1 小时前
Django 入门指南:构建强大的 Web 应用程序
前端·django·sqlite