前端实用技能,同时拖拽文件和文件夹上传控件实现!

作者:Zayn

原文:https://juejin.cn/post/7340539852712804387

最近业务中遇到有关上传文件的需求,其中有涉及到支持同时上传文件和文件夹的功能,Input[type=file]要么只上传文件,要么加webkitdirectory只上传文件夹,没办法同时上传文件和文件夹,后面了解到可以通过拖拽实现同时将文件和文件夹拉到捕获区获取文件.

实现结果

可以点击上传文件,或者点击上传文件夹,但只能二选其一,如果是拖拽,可以将文件和文件夹同时拖拽进来。

5cd8112aa761da210576e7bedfd57f8f.jpeg

点击上传

这里我有用到vue3的一些api,需要的可以结合自己的技术栈修改一下。

<template>
  <div class="c-fileUpload-container" ref="fileUploadRef" @click="handleFileClick">
    <input type="file" style="display: none" :multiple="multiple" :accept="accept" ref="fileInputRef" />
    <input type="file" webkitdirectory mozdirectory odirectory style="display: none" ref="directoryInputRef" />
    <div class="c-fileUpload-container-desc">
      <slot>
        <p class="c-fileUpload-drag-icon">
          <cloud-upload-outlined />
        </p>
        <p class="c-fileUpload-text">
          {{ noDragDropSupport ? '浏览器不支持拖拽上传,' : `将文件${directory ? '/文件夹' : ''}  拖到此处,或 ` }}
           <a @click.stop="handleFileClick">点击上传文件</a>
          <template v-if="directory">
            或 <a style="z-index: 3" @click.stop="handleDirectoryClick">点击上传文件夹</a>
          </template>
        </p>
      </slot>
    </div>
  </div>
</template>

js:

const handleFileChange = e => {
  // 获取文件
  const files = e.target.files
  // 遍历文件,单个文件调用handleFile方法
  Array.from(files).map(file => {
    // 处理文件
  })
}
const handleDirectoryChange = e => {
  // 获取文件夹
  // 遍历文件,单个文件调用handleFile方法
  const files = e.target.files
  Array.from(files).map(file => {
   // 处理文件
  })
}
const handleFileClick = () => {
  fileInputRef.value.click()
}
const handleDirectoryClick = () => {
  directoryInputRef.value.click()
}

onMounted(()=>{
    fileInputRef.value.addEventListener('change', handleFileChange)
    directoryInputRef.value.addEventListener('change', handleDirectoryChange)
})

webkitdirectory mozdirectory odirectory 为了兼容性选择文件夹

webkitdirectory为chrome

mozdirectory为firefox

odirectory为opera

拖拽同时上传文件和文件夹

兼容性检查

// 检查浏览器是否支持拖放
function checkDragDropSupport() {
  const div = document.createElement('div');
  return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
}
const noDragDropSupport = !checkDragDropSupport()
if(!noDragDropSupport){
    // 提醒用户当前浏览器版本不支持拖拽文件
}

拖拽过程样式

可以通过监听dragover/dragEnter和dragLeave实时改变拖拽区域的样式,增强用户交互效果,如改变border等

const handleDragLeave = e => {
  e.preventDefault()
  e.stopPropagation()
  fileUploadRef.value.style.border = '1px dashed #d9d9d9'
}
const handleDragOver = e => {
  e.preventDefault()
  e.stopPropagation()
  // 拖拽时边框变色,同时保证性能
  fileUploadRef.value.style.border = '1px dashed #6bbcff'
}

onMounted(()=>{
    fileUploadRef.value.addEventListener('dragleave', handleDragLeave)
    fileUploadRef.value.addEventListener('dragover', handleDragOver)
})
effectAllowed和 dropEffect

可以通过调整拖拽释放区的这两个效果来控制文件进去之后的鼠标样式,默认为copy

dropEffect 表示拖放操作的视觉效果,effectAllowed 用来指定当元素被拖放式所允许的视觉效果

dropeffect可取值:none|copy|link|move

effectAllowed可取值:copy|move|link|copyLink|copyMove|linkMove|all|none|uninitialized

获取文件

这是重中之重,通过onDrop事件,我们可以获取到dataTransfer获取到相关的文件和文件夹,但此时他们不是File对象,我们需要转换,及递归将文件夹中的文件获取。

const handleDrop = e => {
  e.preventDefault()
  e.stopPropagation()
  fileUploadRef.value.style.border = '1px dashed #d9d9d9'
  // e.dataTransfer.items是一个DataTransferItemList对象,包含了拖拽的文件
  getFilesFromDataTransferItemList(e.dataTransfer.items)
}
onMounted(()=>{
    fileUploadRef.value.addEventListener('drop', handleDrop)
})

const getFilesFromDataTransferItemList = items => {
  const dfsForDirectory = async item => {
    if (item.isFile) {
        // 文件
      const file = await readFileEntrieQueue(item)
      emit('handleFile', file)
    } else {
        //  文件夹
        // 获取文件夹中的文件entry
      const entries = await readDirEntrieQueue(item)
      for (let entry of entries) {
          // 递归获取文件
        dfsForDirectory(entry)
      }
    }
  }
  for (let i = 0; i < items.length; i++) {
  // webkitGetAsEntry()方法返回一个FileSystemEntry对象,表示DataTransferItem对象的文件系统条目
    dfsForDirectory(items[i].webkitGetAsEntry())
  }
}
const readDirEntrieQueue = createQueue(20, entery => {
  return new Promise((resolve, reject) => {
  // 读取文件夹,返回一个FileSystemDirectoryReader对象,该对象表示一个目录的内容,可以通过readEntries()方法读取目录的内容
    entery.createReader().readEntries(entries => {
      resolve(entries)
    })
  })
})
const readFileEntrieQueue = createQueue(20, entery => {
    // 读取文件
  return new Promise((resolve, reject) => {
    entery.file(file => {
      resolve(file)
    })
  })
})

这里我用了队列来控制读取文件和文件夹内容的任务,因为业务里需要同时拖拽几百个文件,如果一下子全部读取会造成卡顿,所以这里最好用队列控制一下。

分享一下我的队列函数

/**
 * 队列操作
 * @param concurrency 同时执行的数量
 * @param fn 操作函数 异步函数
 * @param fn.dataItem 操作的数据
 * @param fn.getRemoveQueue 撤销排队中的某一个任务
 * @returns {function(*=, *=): Promise<unknown>}
 * 使用方法
 * let removeFn = null
 * const getRemoveFn = fn => removeFn = fn
 * const handleDataByQueue = createQueue(3, async (dataItem) => {
 *  await sleep(1000)
 *  console.log(dataItem)
 *  return dataItem
 *  })
 *  handleDataByQueue(1, getRemoveFn)
 *  // 取消排队
 *  setTimeout(() => {
 *  removeFn && removeFn()
 *  }, 1000)
 * */
export const createQueue = (concurrency, fn) => {
  const queue = []
  const runningQueue = []
  // 撤销排队中的某一个任务

  const removeQueue = task => {
    const index = queue.findIndex(item => item === task)
    if (index !== -1) {
      console.log('取消排队')
      queue.splice(index, 1)
    }
  }
  const process = (dataItem, getRemoveQueue) => {
    return new Promise((resolve, reject) => {
      const run = async () => {
        if (runningQueue.length >= concurrency) {
          queue.push(run)
          getRemoveQueue && getRemoveQueue(() => removeQueue(run))
          return
        }
        runningQueue.push(run)
        try {
          const result = await fn(dataItem)
          resolve(result)
        } catch (e) {
          reject(e)
        } finally {
          runningQueue.splice(runningQueue.indexOf(run), 1)
          if (queue.length) {
            queue.shift()()
          }
        }
      }
      run()
    })
  }
  return process
}

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我个小忙:

  1. 点个「喜欢」或「在看」,让更多的人也能看到这篇内容

  2. 我组建了个氛围非常好的前端群,里面有很多前端小伙伴,欢迎加我微信「sherlocked_93」拉你加群,一起交流和学习

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

8b2a859eb3a940bd87d8f0fe79c62adb.png

1ad93c1299113428c13efdb6a3297162.png

点个喜欢支持我吧,在看就更好了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值