前端拖拽相关功能详解,一篇文章总结前端关于拖拽的应用场景和实现方式(含源码)

前言

本篇文章所有的代码,都是在 vue + vite + ts 项目基础之上实现的,这样也是为了方便大家直接用源码,在开始之前建议大家阅读这篇《零基础搭建 vite项 目教程》。此项目就是这个教程搭建的,本篇文章关于拖拽的相关代码是此项目的一个分支。如果你没有时间阅读详细的教程,你也可以直接在 git 上克隆项目。

learn-vite: w搭建简单的vite+ts+vue框架

本篇文章的所有代码都在 drag 分支上,基本内容大致如下(后续代码可能会有优化):

把项目克隆之后切换到 drag 分支,运行 npm run dev 直接访问 http://localhost:80/drag  就可以看到本篇文章涉及的全部内容。

我将拖拽的功能大致分为五大类,分别是(1)拖拽上传、(2)拖拽调整宽度、(3)拖拽调整按钮位置、(4)拖拽排序、(5)拖拽将文件移动到文件夹。这些都是比较常用的,除此之外其实还有很多复杂的情况,后续会再优化和补充。

一、文件拖拽到指定区域上传

文件拖拽上传,是一个很基础的功能,一般来说我们有两种选择,(1)使用组件库,(2)使用自己封装的方法。

1.1 组件库实现拖拽上传

常见的组件库,比如 elementPlus 、antDesignVue 等都提供上传组件。

使用组件库的好处是,简单、安装即用,组件库一般都提供很成熟的方法和各种事件的回调,免得我们再自定义。组件库的拖拽上传本质上就是利用了 js 的 drag、drop 等事件来实现的。

但是组件库的缺点也显而易见:

(1)会影响页面的 html 结构

因为我们在使用的时候,需要在页面增加一个 elUpload 标签,然后页面上所有的内容都需要包裹在这个 elUpload 标签内,页面的结构和布局都受这个标签的影响。

(2)样式需要自定义

组件库的样式很多时候不是我们想要的,还需要对它的样式进行修改自定义,徒增工作量。

我认为修改组件库的样式是一件很麻烦的事情,因为组件库的样式过于全面,对于很多操作比如 hover、focus、active 都有对应的样式。但是实际上我们的需求根本不用考虑这么多,所以在使用组件库的时候,样式覆盖的要考虑得很全面,还要考虑样式的层级关系,动不动就需要加一些 !important ,也很容易出现问题。

所以我在实际开发过程中,如果是小功能能不用组件库就不用,比如一个简单的输入框,我选择自定义 input 标签,而不是使用 el-input。

但是有些时候还是要用的,比如一个 popover 功能,我们需要它自动定位的时候就直接用 el-popover 比较好,因为计算它的位置是一个很麻烦的事情,也就是人们常说的不要自己造轮子。

总之,能找到对于自己来说更高效的开发方法就好。

对于一些新增的功能还好,可以使用组件库提高工作效率;但是如果在一个现有的旧页面中,增加上传功能,就不推荐使用组件库,因为我们最好不要改变页面原本的 dom 结构。

解决办法就是我们自定义一个拖拽上传方法。

(3)代码

我的这个项目用的组件库是 ant-design-vue ,但是文档中是用 elementPlus 举例的,实际上都是大同小异。

<template>
	<div class="drag-upload-container">
		<h1 class="title">使用组件库实现拖拽上传文件</h1>
		<!-- 使用组件库实现拖拽上传,需要在模版中增加一个标签,入下面的 a-upload-draager -->
		<a-upload-dragger v-model:fileList="fileList" name="file" :multiple="true" action="https://www.mocky.io/v2/5cc8019d300000980a055e76" @change="handleChange" @drop="handleDrop">
			<p class="ant-upload-text">Click or drag file to this area to upload</p>
			<p class="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>
		</a-upload-dragger>
	</div>
</template>
<script lang="ts" setup>
import { UploadDragger as AUploadDragger } from 'ant-design-vue'
import { ref } from 'vue'

const fileList = ref([])
const handleChange = () => {
	//
}
const handleDrop = () => {
	//
}
</script>
<style lang="scss" scoped>
.drag-upload-container {
	padding: 24px;
	.title {
		margin: 10px 0 20px;
	}
}
</style>

本小结的源代码在项目中的 src/pages/drag/dragUpload.vue  文件夹中。

1.2 自己封装拖拽上传方法

为了解决 1.1 节组件库影响 dom 结构的问题,我们需要自己封装一个拖拽上传方法,宗旨是:

  1. 不论页面结构多复杂,都不要影响原来的 dom 结构。
  2. 一个封装的公共类,复制到任何项目都可以使用

我们要知道拖拽的本质就是利用了 javascript 的 drag/drop 等事件,开始之前耐心的看一下 mdn 官方关于 HTML 拖拽 API 的介绍。HTML 的拖拽 API

拖放的主要步骤是 drop 事件定义的一个释放区(释放文件的目标元素)和为 dragover 事件定义一个事件处理程序。

(1)基本步骤

  1. 定义拖放区域,用于触发 drop 事件
  2. 给拖放区域定义 drapover 事件和样式,用于指示用户此处可以拖放文件
  3. 对 drop 事件的详细定义,文件的获取和处理

我们在封装类的时候,需要把拖放区域的 dom 当作一个参数传入,这样就可以在任何地方复用。需要用到的所有事件有:

  1. dragenter: 拖拽进入【拖放区域】,此时可能要展示某些提示文案或样式
  2. dragover: 在【拖放区域】中,此时可能要展示某些提示文案或样式
  3. dragleave: 拖拽离开【拖放区域】,此时可能要隐藏某些提示文案或样式
  4. drop: 在【拖放区域】松开鼠标,此时要获取我们拖拽的文件,并进行后续的处理

除了上面 4 个事件,还有 drag、 dragstart 、dragend 三个事件是我们在当前功能用不到的,因为这三个事件是针对【被拖拽元素】的,拖拽上传功能中,【被拖拽元素】是我们电脑文件系统中的某个文件。

如果【被拖拽元素】是我们当前页面的某个 dom 元素,那么这几个事件就有用了,在本篇文章的第五章【拖拽将文件移动到文件夹】中有用到。

(2)使用 dataTransfer 传递数据

DataTransfer 接口是一个 HTML 原生接口,用于保存拖动并放下过程中的数据,他可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型

我们只需要在 drop 事件中取 event.dataTransfer.files 就可以获取到我们正在拖拽的文件

// 一个 drop 事件
function onDrop(evt: DragEvent) {
    // 获取到拖拽的文件
	const res = evt.dataTransfer?.files
}

(3)事件监听

还有一个重要的问题,我们在监听 drop、dragover 等事件的时候,应该是监听我们的【拖放区域】元素的事件,而不是 document 等。在本例中,我们将【拖放区域】元素作为一个参数传递给封装的类,这样功能就可以复用了。

(4)代码

下面是我封装的一个 DragUploader 类,在这个类中,我们把拖拽中的【提示文案 tipText】通过参数传入,并在类的内部创建并设置对应的 dom 元素的样式(tipEle)。实际上,我们也可以直接把拖拽中的样式的整个 dom 作为参数传入,看具体需求和开发习惯。

export class DragUploader {
	// 拖拽区域的父元素
	el: HTMLElement | null = null
	// 拖拽中的文案
	tipText: string = ''
	// 拖拽中展示的元素
	tipEle: HTMLElement | null = null
	// 拖拽上传文件之后的回调
	fileCb: (files: Array<File>) => void

	constructor({ el, tipText, fileCb }: { el: HTMLElement; tipText: string; fileCb: (Files: Array<File>) => void }) {
		this.el = el
		this.tipText = tipText
		// 创建拖拽中展示的元素
		this.tipEle = this.createTipEle(tipText)
		// 获取文件后的回调,以供后续使用
		this.fileCb = fileCb
		// 监听拖拽事件,创建实例的时候就开始监听
		this.addListener(el)
	}

	createTipEle(tipText: string) {
		const ele = document.createElement('div')
		// 自定义样式,使用绝对定位,需要设置根元素的 position
		ele.style.position = 'absolute'
		ele.style.top = '0px'
		ele.style.left = '0px'
		ele.style.display = 'none'
		ele.style.alignItems = 'center'
		ele.style.justifyContent = 'center'
		ele.style.width = '100%'
		ele.style.height = '100%'
		ele.style.background = '#fff'
		ele.style.pointerEvents = 'none' // 必须加上这句,否则会重复触发 drag事件
		ele.innerHTML = tipText
		return ele
	}
	// 显示拖拽中
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我有一棵树

感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值