携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情
前端开发中,常见的文件上传场景有:
- 单个文件上传
- 多个文件上传
- 目录上传
- 拖拽上传
- 剪贴板上传
- 大文件分块上传
本篇我们来讲一下拖拽上传文件的场景
往期回顾:
使用拖拽上传的话,主要是前端部分要改成拖拽的方式上传文件,后端实际上不需要作出修改,那么前端需要作出什么修改呢?
我们需要先了解一下拖拽相关的api,看看如何实现拖拽,不过暂时不急,我们先把基础的页面搭建好先
搭建基础页面
首先搭建一个可拖拽的文件上传页面出来
<template>
<div
flex-col
flex-center
w-xl
h-sm
border-1
border-dashed
rounded-3
border-gray-6
cursor-pointer
hover:border-cyan
transition-200
>
<div i-carbon:cloud-upload text-16></div>
<span>Drop file here or <span text-cyan>click to upload</span></span>
</div>
</template>
效果如下:
抽离文件上传逻辑为hooks
首先添加上点击上传文件的功能,这个可以直接复用之前单文件多文件上传时的组件中的逻辑,所以我们可以先将之前的上传逻辑抽离成hooks
src/hooks/useFileUploader.ts
import { multipleUploadFile, singleUploadFile, uploadDirectory } from '~/api'
interface Options {
// 是否进行目录上传
directory?: boolean
// formData 的 fieldName
fieldName?: string
}
export default function useFileUploader(options?: Options) {
const directory = options?.directory ?? false
const fieldName = options?.fieldName ?? 'file'
return (files: FileList | undefined | null) => {
// 没选择文件则不调用接口
if (!files || files.length === 0) return
// 开启目录上传的话则只调用目录上传接口
if (directory) {
uploadDirectory(fieldName, files)
return
}
if (files.length > 1) {
// 超过 1 个文件则用多文件上传
multipleUploadFile(fieldName, files)
} else {
// 获取选择的第一个文件并调用接口上传
if (files.item(0) !== null) {
singleUploadFile('file', files.item(0) as File)
}
}
}
}
然后替换掉前面我们的文件上传组件的相应逻辑代码
// 文件上传表单元素
const uploadFileRef = ref<HTMLInputElement>()
const uploadFile = useFileUploader({ directory: props.webkitdirectory })
const handleInputFileConfirm = () => {
if (uploadFileRef.value?.files) {
uploadFile(uploadFileRef.value?.files)
}
}
这样上传文件的逻辑就可以直接在新的拖拽上传文件组件中复用了,是不是特别方便呢!
将上传文件逻辑复用到新组件中
由于我们已经抽离了上传文件的逻辑了,现在直接复用即可
<script setup lang="ts">
import { useFileUploader } from '~/hooks'
interface Props {
// 是否多文件上传
multiple?: boolean
// 上传文件的 MIME type
mime?: string
// 是否要开启目录上传 -- 非标准特性 有兼容性问题
webkitdirectory?: boolean
}
// withDefaults 可以解决类型声明 props 类型的时候无法声明默认值的问题
const props = withDefaults(defineProps<Props>(), {
// 默认单文件上传
multiple: false,
// 默认只支持上传图片
mime: 'image/*',
// 默认不开启目录上传
webkitdirectory: false,
})
const uploadFileRef = ref<HTMLInputElement>()
const uploadFile = useFileUploader({ directory: props.webkitdirectory })
</script>
<template>
<input
display-none
:multiple="multiple"
:accept="mime"
:webkitdirectory="webkitdirectory"
type="file"
ref="uploadFileRef"
@input="uploadFile(uploadFileRef?.files)"
/>
<div
flex-col
flex-center
w-xl
h-sm
border-1
border-dashed
rounded-3
border-gray-6
cursor-pointer
hover:border-cyan
transition-200
@click="uploadFileRef?.click()"
>
<div i-carbon:cloud-upload text-16></div>
<span>Drop file here or <span text-cyan>click to upload</span></span>
</div>
</template>
这样一来点击上传文件的逻辑就和之前一样了,接下来我们可以开始实现拖拽上传的逻辑了
使用自定义指令让元素变成可放置
我们现在要让虚线区域的元素变成拖拽元素的可放置区域,需要阻止几个拖拽事件的默认行为,才能让虚线区域的元素变成可放置元素
可以使用vue的事件修饰符.stop和.prevent去完成,但是由于有四个事件dragenter、dragover、dragleave和drop都要设置,也就意味着通过声明式的写法的话,我们得写八个这样的事件修饰符,显得有些冗余
这时候就可以让自定义指令发挥用处了,可以自定义一个v-droppable指令,将应用到的元素的这四个事件的默认行为阻止掉,并阻止它们冒泡
src/directives/vDroppable.ts
import type { Directive } from 'vue'
/**
* @description 让元素变得可放置
*/
export const vDroppable: Directive<HTMLElement> = (el, binding) => {
const preventDefaultAndStopPropagation = (e: Event) => {
e.preventDefault()
e.stopPropagation()
}
;(
[
'dragenter',
'dragover',
'dragleave',
'drop',
] as (keyof HTMLElementEventMap)[]
).forEach(eventName => {
el.addEventListener(eventName, preventDefaultAndStopPropagation, false)
})
}
为方便看到效果,我们先弄一个draggable的元素在页面上,然后尝试将它拖拽到上传区域中
<div w-30px h-30px bg-cyan draggable></div>
可以看到,并不能将元素拖拽进去,那么接下来将我们的v-droppable自定义指令套用上去后呢?
<div
...
+ v-droppable
>
<div i-carbon:cloud-upload text-16></div>
<span>Drop file here or <span text-cyan>click to upload</span></span>
</div>
可以看到,上传区域现在变成了可放置元素了,接下来我们再实现以下元素拖进来时,让上传区域高亮的效果
检测到元素被拖拽进入时让上传区域高亮
这个功能我们只需要在dragenter和dragover的时候给上传区域元素添加一个highlight类名,并为其编写样式即可让其高亮,而dragleave和drop的时候将highlight类名移除即可
这完全可以作为v-droppable自定义指令的特性,我们可以通过指令修饰符的方式,添加一个highlight修饰符来实现,而为了提高自定义能力,我们可以允许用户自定义高亮时添加的类名,这个可以通过自定义指令的value实现,代码如下
import type { Directive } from 'vue'
type EventNameList = (keyof HTMLElementEventMap)[]
const preventDefaultAndStopPropagation = (e: Event) => {
e.preventDefault()
e.stopPropagation()
}
/**
* @description 批量注册事件监听器
* @param eventNameList 事件名列表
* @param handler 事件处理函数
*/
const batchAddEventListener = (
el: HTMLElement,
eventNameList: EventNameList,
handler: (...args: any[]) => any,
useCapture?: boolean,
) => {
useCapture = useCapture ?? false
eventNameList.forEach(eventName =>
el.addEventListener(eventName, handler, useCapture),
)
}
// 让元素变得可放置
const makeElDroppable = (el: HTMLElement) => {
batchAddEventListener(
el,
['dragenter', 'dragover', 'dragleave', 'drop'],
preventDefaultAndStopPropagation,
)
}
/**
* @description 有可拖拽元素进入时让可放置元素高亮
* @param el 元素
* @param highlightClassName 高亮时添加的类名
*/
const makeElHighlight = (el: HTMLElement, highlightClassName: string) => {
// 进入和移动时添加 highlightClassName
batchAddEventListener(el, ['dragenter', 'dragover'], () => {
el.classList.add(highlightClassName)
})
// 离开和放下时移除 highlightClassName
batchAddEventListener(el, ['dragleave', 'drop'], () => {
el.classList.remove(highlightClassName)
})
}
/**
* @description 让元素变得可放置
*/
export const vDroppable: Directive<
HTMLElement,
{ highlightClassName?: string } | undefined
> = (el, binding) => {
// 默认不高亮
const highlight = binding.modifiers?.highlight ?? false
// 高亮时添加的类名 -- 交给外界自定义类名及其样式 提高通用性
const highlightClassName = binding.value?.highlightClassName ?? 'highlight'
makeElDroppable(el)
console.log(highlight)
if (highlight) {
makeElHighlight(el, highlightClassName)
}
}
接下来我们在使用自定义指令的地方加上v-droppable.highlight修饰符即可启用高亮功能
<div
...
v-droppable.highlight
>
<div i-carbon:cloud-upload text-16></div>
<span>Drop file here or <span text-cyan>click to upload</span></span>
</div>
如果希望自定义高亮时添加的类名,就给它赋值
<div
...
v-droppable.highlight="{ highlightClassName: 'custom-highlight' }"
>
<div i-carbon:cloud-upload text-16></div>
<span>Drop file here or <span text-cyan>click to upload</span></span>
</div>
然后为.custom-hightlight类名定义样式,我们模仿element-plus的拖拽上传组件样式
.custom-highlight {
background: rgba(64, 158, 255, 0.2);
border-color: rgb(64, 158, 255);
}
效果如下:
接下来只要给拖拽区域元素的drop事件触发时执行uploadFile即可
<div
...
@drop="e => uploadFile(e.dataTransfer?.files)"
>
现在我们来拖拽两张图片进来看看
可以看到成功上传,至此拖拽上传功能就实现啦!