[可乐的随手记 - 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()
  }
}
相关推荐
缘友一世几秒前
将现有Web 网页封装为macOS应用
前端·macos·策略模式
刺客-Andy17 分钟前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
谢道韫66622 分钟前
今日总结 2024-12-27
开发语言·前端·javascript
嘤嘤怪呆呆狗32 分钟前
【插件】vscode Todo Tree 简介和使用方法
前端·ide·vue.js·vscode·编辑器
大今野38 分钟前
node.js和js
开发语言·javascript·node.js
ᥬ 小月亮1 小时前
Js前端模块化规范及其产品
开发语言·前端·javascript
码小瑞1 小时前
某些iphone手机录音获取流stream延迟问题 以及 录音一次第二次不录音问题
前端·javascript·vue.js
weixin_1891 小时前
‌Vite和Webpack区别 及 优劣势
前端·webpack·vue·vite
半吊子伯爵1 小时前
开发过程优化·自定义鼠标右键菜单
前端·javascript·自定义鼠标右键菜单
xcLeigh1 小时前
HTML5实现好看的喜庆圣诞节网站源码
前端·html·html5