面试必备,学会使用 Vue3 + ts 手写实现拖拽hook
面试不用慌,写给初中级前端的高级进阶,读源码,多动手,合理利用摸鱼时间,提高自己的编码水平,本期带给大家Vue3 + ts 手写实现拖拽hook,收藏关注加点赞,是我更新的动力!
先看成果
拖拽
拖拽的实现是比较常见的功能,现在很多的插件都能够实现,为了更好的熟练vue3和ts,笔者通过手写hooks的方式来对拖拽的功能进行实现。
页面UI部分
笔者需要实现一个hook,这个hook的功能是传入一个element和初始化坐标,实现对element的可拖拽,并且返回拖拽的实时坐标,下面的UI页面的代码
<template>
<div ref="el" :style="style" style="position: fixed;border: 1px solid #409EFF;">
我的坐标: {{ x }}, {{ y }}
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from '../hooks/useDraggable'
const el = ref<HTMLElement | null>(null)
const { x, y, style } = useDraggable(el, {
initPosition: { x: 40, y: 40 },
})
</script>
这里我们需要的是xy的坐标,所以需要我们的hook来进行返回
同时由于是拖拽,我们肯定是需要element元素的坐标进行移动的,我们先设置好position的定位,然后根据hook返回的left和top来实现拖拽
hook实现
1.处理传参和返回
我们接收一个dom元素和一个options的配置参数,由于我们可以通过ref的方式来传入dom或者是真实的dom,因为我们是通过ref来获取dom再传入的,所以这里的el需要用ts做一个处理,使用ref传入的类型和真实的dom的类型不同
这里可以看一下效果
ref获取的节点
真实的节点
下面我们用ts来对传入的参数做一个限制
2.参数ts处理
type MaybeRef<T> = T | Ref<T>
这里用到了泛型,MaybeRef的作用则是,处理传入的dom可能是HTMLElement类型
也可能是ref<HTMLElement | null>类型
HTMLElement类型
是真实的dom
ref<HTMLElement | null>类型
是通过ref获取到的dom
然后对传入的配置做一个限制,这里先简单的进行定义dragOptions
type MaybeRef<T> = T | Ref<T>
interface dragOptions {
initPosition?: Position
}
export function useDraggable(el: MaybeRef<HTMLElement | undefined | null>, options: dragOptions = {}) {
}
3.监听拖拽事件实现
通过getBoundingClientRect我们可以获取到元素的这些信息,如图

3.1ts加强
由于传入的el元素有多种类型,如果是ref,那么则需要用到el.value来获取dom,但是笔者在写的时候忽略了另外一张el的类型根本就不存在value的值,所以这个地方出现了提示,这也恰恰说明了ts在编码过程中的好处,可以提前发现隐藏的问题,及时解决。
解决方案
既然参数el可能是Htmlelement或者是ref 那么就利用ts来进行约束判断
这里利用泛型来进行书写,如果是ref,则返回el.value
function toValue<T>(el:MaybeRef<T>):T{
if(isRef(el)){
return el.value
}
return el
}
3.2 拖拽监听实现
isDragging
来判断拖拽的开始和结束
tempPosition
来保存拖拽的距离
position
保存初始xy位置和拖动的xy位置
具体的计算这里就不详细说了,原理比较简单,通过代码可得
import { useEventListener } from '@vueuse/core'
interface Position {
x: number
y: number
}
let isDragging = false; // 是否正在拖动
const tempPosition = ref<Position>(
{ x: 0, y: 0 }
)
const position = reactive<Position>(
{ x: 0, y: 0 }
)
if (options.initPosition) {
position.x = options.initPosition.x
position.y = options.initPosition.y
}
const start = (e: PointerEvent) => {
isDragging = true
let { left, top } = toValue(el)!.getBoundingClientRect()
tempPosition.value.x = e.clientX - left
tempPosition.value.y = e.clientY - top
}
const move = (e: PointerEvent) => {
if (!isDragging)
return
position.x = e.clientX - tempPosition.value.x
position.y = e.clientY - tempPosition.value.y
}
const end = (e: PointerEvent) => {
isDragging = false
tempPosition.value.x = 0
tempPosition.value.y = 0
}
useEventListener(el, 'pointerdown', start,true)
useEventListener(window, 'pointermove', move,true)
useEventListener(el, 'pointerup', end,true)
3.3 注意move的监听方式
如果move事件加到整个el上,监听事件跟不上鼠标的移动,从而鼠标移出监听的元素,效果如下
对此我们需要进行一些修改,把move的监听事件放到整个window上,这样的拖动效果会更加顺滑
处理返回
页面上我们需要用到的有两点,一个是xy的具体位置,另外一个是元素的偏移
元素的偏移我们用到computed来计算style进行返回
return {
..._position,
style: computed(
() => `left:${position.x}px;top:${position.y}px;cursor:move;`,
),
}
对于position,我想通过解构的方式传出,但这里需要解决一些问题
如果单纯的把position解构出来,那么可以看到x和y都不是响应式的,所以不能这么用
let _position = toRefs(position)
return {
// ...position,
..._position,
style: computed(
// () => console.log(position.x)
() => `left:${position.x}px;top:${position.y}px;cursor:move;`,
),
}
需要用到toRefs把我们的Postion里面的值也转成响应式,然后再进行解构
这样xy才能在页面上实现响应式。
代码完善
浏览器默认事件处理
按照下面的代码 每次拖拽的时候都会由于触发了冒泡行为而进行打印
<div @click="console.log('冒泡')" :style="style1" style="position: fixed;border: 1px solid #409EFF;height: 100px;width: 200px;" >
<div style="color:#409EFF;" ref="el1" >👋 点我</div>
<div style="color:red;margin-top: 20px;">此处无法拖拽</div>
</div>
同时可能存在图片等默认的拖拽行为,我们也可以给防止,这里声明一个handleEvent
const handleEvent = (e: PointerEvent) => {
e.preventDefault()
e.stopPropagation()
}
const start = (e: PointerEvent) => {
handleEvent(e)
}
const move = (e: PointerEvent) => {
handleEvent(e)
}
const end = (e: PointerEvent) => {
handleEvent(e)
}
"🙏 感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请关注我的更新,给个喜欢和分享。您的支持是我写作的最大动力!✍️🌟"
往期好文推荐
转载自:https://juejin.cn/post/7257285697395752997