最近业务中遇到有关上传文件的需求,其中有涉及到支持同时上传文件和文件夹的功能,Input[type=file]要么只上传文件,要么加webkitdirectory只上传文件夹,没办法同时上传文件和文件夹,后面了解到可以通过拖拽实现同时将文件和文件夹拉到捕获区获取文件.
实现结果
可以点击上传文件,或者点击上传文件夹,但只能二选其一,如果是拖拽,可以将文件和文件夹同时拖拽进来。
点击上传
这里我有用到vue3的一些api,需要的可以结合自己的技术栈修改一下。
xml
<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:
javascript
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
拖拽同时上传文件和文件夹
兼容性检查
javascript
// 检查浏览器是否支持拖放
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等
scss
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对象,我们需要转换,及递归将文件夹中的文件获取。
javascript
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)
})
})
})
这里我用了队列来控制读取文件和文件夹内容的任务,因为业务里需要同时拖拽几百个文件,如果一下子全部读取会造成卡顿,所以这里最好用队列控制一下。
分享一下我的队列函数
javascript
/**
* 队列操作
* @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
}
如有不妥,多多指教!