前端伪队列实现文件顺序上传功能

12 阅读3分钟

需求提要

通过两种方式进行文件上传:【直接上传】和【分步上传】

直接上传

字面意思,即点击了上传,一直到最后上传结束,整个流程没有阻碍

分步上传

点击上传后,流程会在中途暂停,等其他条件触发时才会继续后续的上传流程

【注意】:不管是直接上传还是分步上传,文件的上传都需要排队(即等前一个任务完成之后,才能继续下一个任务)

编码分析

采用队列数据结构

  • 使用 fileList 作为队列存储容器,用来存放待上传的文件
  • 文件的处理遵循先进先出(FIFO)的原则(队列的基本特征)

队列状态管理

  • 通过几种关键状态来管理队列

    • isUploading 标志:表示队列是否正在处理中
    • currentFile:记录当前正在处理的文件
    • 文件状态追踪(WAITINGWAITING_CALLBACKSUCCESS

队列操作

  • 入队:通过 uploadClick 函数添加新文件到队列
  • 处理:通过 processUpload 函数逐个处理文件
  • 出队:文件状态变为 SUCCESS 时相当于完成出队

顺序处理

  • 创建一个可控的 Promise,能够在 Promise 外部控制其状态(resolvereject

    // 工具函数 - 创建全局可访问的promise
    const createDeferred = () => {
      let resolveCallback, rejectCallback
      const promise = new Promise((resolve, reject) => {
        resolveCallback = resolve,
        rejectCallback = reject
      })
      return { promise, resolve: resolveCallback, reject: rejectCallback }
    }
    
    // # ======= 使用 ======= #
    const deferred = createDeferred()
    
    // 在某个时间点触发完成
    promise setTimeout(() => { 
      deferred.resolve('上传完成');
    }, 1000);
    
    // 等待promise完成
    await deferred.promise;
    
  • 通过 await currentFile.value.deferred.promise 实现异步控制和同步等待

  • 它会暂停当前的上传流程,等待当前文件完成上传完成后(调用 currentFile.value.deferred.resolve() )才继续执行后续逻辑

编码实现

<template>
  <div class="queue-demo-module">
    <div class="btn-group">
      <div
        class="btn"
        v-for="item in uploadButtons"
        :id="item.id"
        @click="uploadClick(item.type)"
      >{{ item.text }}</div>
    </div>
    <div>
      <ul>
        <li v-for="item of fileList" :id="item.id">
          <div>{{ item.name }}</div>
          <div class="percent">{{ item.uploadProgress }}</div>
        </li>
      </ul>
    </div>
  </div>
</template>
<script>
import { defineComponent, ref } from '@vue/composition-api'
import { nanoid } from 'nanoid'

const UPLOAD_STATUS = {
  WAITING: 1, // 等待上传
  WAITING_CALLBACK: 2, // 等待回调后上传
  SUCCESS: 3, // 上传完成
}

export default defineComponent({
  name: 'QueueDemo',
  setup () {
    const fileList = ref([]) // 文件队列
    const isUploading = ref(false) // 是否有在上传文件的队列在进行
    const currentFile = ref({}) // 当前上传的文件对象
    const uploadButtons = [
      { id: 1, type: 'direct', text: '直接上传' },
      { id: 2, type: 'callback', text: '分步上传' },
    ]

    // 上传过程 -【核心函数】
    const processUpload = async () => {
      if (isUploading.value) return

      // 过滤待上传文件列表
      const pendingFile = fileList.value.find(item => [UPLOAD_STATUS.WAITING, UPLOAD_STATUS.WAITING_CALLBACK].includes(item.uploadStatus))
      
      // 如果没有待上传文件,队列结束
      if (!pendingFile) {
        isUploading.value = false
        return
      }

      // 有,则标识队列进行
      isUploading.value = true
      const deferred = createDeferred() // 获取 deferred 对象
      // 处理当前要上传的文件对象
      currentFile.value = { ...pendingFile, deferred }

      try {
        if (currentFile.value.uploadStatus === UPLOAD_STATUS.WAITING) {
          await handleProgress(currentFile.value, 100)
          currentFile.value.deferred.resolve() // 同步上传完成
        } else {
          await handleProgress(currentFile.value, 50)
          setTimeout(() => {
            handleCallback() //【模拟分步上传的回调】
          }, 1000);
        }

        // 触发一个全局promise等待,确保在文件真正上传完成(包括分步上传的回调)后才继续处理下一个文件
        await currentFile.value.deferred.promise

        console.warn(' == upload finish == ')

        // 更新状态
        fileList.value = fileList.value.map(file => {
          return file.id === currentFile.value.id
          ? { ...file, uploadStatus: UPLOAD_STATUS.SUCCESS }
          : file
        })

        // 寻找下一个待上传的文件对象
        const hasMorePendding = fileList.value.some(file => [UPLOAD_STATUS.WAITING, UPLOAD_STATUS.WAITING_CALLBACK].includes(file.uploadStatus))

        if (hasMorePendding) {
          isUploading.value = false  // 重置状态后再递归
          await processUpload()
        }
      } catch (error) {
        console.error(' == upload failed:', error)
      } finally {
        if (fileList.value.some(file => [UPLOAD_STATUS.WAITING, UPLOAD_STATUS.WAITING_CALLBACK].includes(file.uploadStatus))) {
          isUploading.value = false
        }
      }
    }

    // 分步上传的回调
    const handleCallback = async () => {
      await updateProgress(currentFile.value, 100)
      currentFile.value.deferred.resolve()
    }

    // 工具函数 - 创建全局可访问的promise
    const createDeferred = () => {
      let resolveCallback, rejectCallback
      const promise = new Promise((resolve, reject) => {
        resolveCallback = resolve,
        rejectCallback = reject
      })
      return { promise, resolve: resolveCallback, reject: rejectCallback }
    }

    // 工具函数 - 睡眠函数
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

    const handleProgress = async (item, targetProgress) => {
      await sleep(500) // 等待500ms
      await updateProgress(item, targetProgress)
    }

    // 初始化上传文件对象
    const createFileObject = (type) => ({
      id: nanoid(),
      name: `File-${nanoid()}-${type}`,
      uploadProgress: 0,
      uploadStatus: type === 'direct' ? UPLOAD_STATUS.WAITING : UPLOAD_STATUS.WAITING_CALLBACK,
    })

    // 更新上传进度
    const updateProgress = (item, targetProgress) => {
      const step = 1
      const interval = 30 // 每50ms更新一次

      return new Promise((resolve) => {
        const timer = setInterval(() => {
          if (item.uploadProgress < targetProgress) {
            item.uploadProgress = Math.min(item.uploadProgress + step, targetProgress)
            fileList.value = fileList.value.map(config => config.id === item.id ? item : config)
          } else {
            clearInterval(timer)
            resolve()
          }
        }, interval)
      })
    }

    // 事件处理
    const uploadClick = async (type) => {
      const obj = createFileObject(type)
      fileList.value.push(obj)
      await processUpload()
    }

    return {
      fileList,
      uploadButtons,
      uploadClick,
    }
  }
})
</script>
<style lang="scss" scoped>
.btn-group {
  .btn {
    width: 160px;
    height: 40px;
    border-radius: 46px;
    text-align: center;
    line-height: 40px;
    background: #FFFFFF;
    border: 1px solid #CCCCCC;
    font-family: Microsoft YaHei;
    font-size: 14px;
    font-weight: 400;
    color: #333333;
    cursor: pointer;
  }
}
ul {
  li {
    display: flex;

    .percent {
      margin-left: 12px;
    }
  }
}
</style>