Vue 3 实现高性能拖拽指令的最佳实践

发布于:2024-11-28 ⋅ 阅读:(16) ⋅ 点赞:(0)

前言

在现代前端开发中,拖拽功能是增强用户体验的重要手段之一。本文将详细介绍如何在 Vue 3 中封装一个拖拽指令(v-draggable),并通过实战例子演示其实现过程。通过这篇教程,您将不仅掌握基础的拖拽功能,还能了解如何优化指令以提升其性能和灵活性,从而为您的项目增色。

封装拖拽指令思路

我们将封装一个简单的拖拽指令,名为 v-draggable,它允许我们在任何元素上添加拖拽功能。
指令逻辑

  1. 监听鼠标事件:我们需要监听 mousedown、mousemove 和 mouseup 事件。
  2. 计算拖动位置:根据鼠标移动的距离更新元素的位置。
  3. 清理事件:在拖动结束后移除事件监听器。

实现步骤

第一步:创建指令文件

在 src 目录下创建一个名为 directives 的文件夹,并在其中创建一个 draggable.js 文件:

// src/directives/draggable.js
export default {
  mounted(el) {
    el.style.position = 'absolute';

    let startX, startY, initialMouseX, initialMouseY;

    const mousemove = (e) => {
      const dx = e.clientX - initialMouseX;
      const dy = e.clientY - initialMouseY;
      el.style.top = `${startY + dy}px`;
      el.style.left = `${startX + dx}px`;
    };

    const mouseup = () => {
      document.removeEventListener('mousemove', mousemove);
      document.removeEventListener('mouseup', mouseup);
    };

    el.addEventListener('mousedown', (e) => {
      startX = el.offsetLeft;
      startY = el.offsetTop;
      initialMouseX = e.clientX;
      initialMouseY = e.clientY;
      document.addEventListener('mousemove', mousemove);
      document.addEventListener('mouseup', mouseup);
      e.preventDefault();
    });
  }
};

第二步:注册指令

在 src 目录下的 main.js 文件中注册这个指令:

import { createApp } from 'vue';
import App from './App.vue';
import draggable from './directives/draggable';

const app = createApp(App);

app.directive('draggable', draggable);

app.mount('#app');

第三步:使用指令

现在我们可以在任何组件中使用这个拖拽指令。编辑 src/App.vue 文件:

<template>
  <div>
    <h1>Vue 3 拖拽指令示例</h1>
    <div v-draggable class="draggable-box">拖拽我!</div>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style>
.draggable-box {
  width: 150px;
  height: 150px;
  background-color: lightblue;
  text-align: center;
  line-height: 150px;
  cursor: move;
  user-select: none;
}
</style>

优化拖拽指令

当前的拖拽指令已经可以基本实现拖拽功能了,但还有一些细节需要优化,例如:

  • 限制拖拽范围
  • 支持触摸设备
  • 添加节流来优化性能
  • 提供一些配置选项

限制拖拽范围

我们可以通过对元素的位置进行限制,来防止其被拖出指定的范围。这里我们假定限制在父元素内进行拖拽。

// src/directives/draggable.js

export default {
  mounted(el) {
    el.style.position = 'absolute';

    let startX, startY, initialMouseX, initialMouseY;

    const mousemove = (e) => {
      const dx = e.clientX - initialMouseX;
      const dy = e.clientY - initialMouseY;

      let newTop = startY + dy;
      let newLeft = startX + dx;

      // 限制拖拽范围在父元素内
      const parentRect = el.parentElement.getBoundingClientRect();
      const elRect = el.getBoundingClientRect();

      if (newLeft < 0) {
        newLeft = 0;
      } else if (newLeft + elRect.width > parentRect.width) {
        newLeft = parentRect.width - elRect.width;
      }

      if (newTop < 0) {
        newTop = 0;
      } else if (newTop + elRect.height > parentRect.height) {
        newTop = parentRect.height - elRect.height;
      }

      el.style.top = `${newTop}px`;
      el.style.left = `${newLeft}px`;
    };

    const mouseup = () => {
      document.removeEventListener('mousemove', mousemove);
      document.removeEventListener('mouseup', mouseup);
    };

    el.addEventListener('mousedown', (e) => {
      startX = el.offsetLeft;
      startY = el.offsetTop;
      initialMouseX = e.clientX;
      initialMouseY = e.clientY;
      document.addEventListener('mousemove', mousemove);
      document.addEventListener('mouseup', mouseup);
      e.preventDefault();
    });
  }
};

支持触摸设备

为了支持触摸设备,我们需要添加 touchstart、touchmove 和 touchend 事件监听器。

// src/directives/draggable.js

export default {
  mounted(el) {
    el.style.position = 'absolute';

    let startX, startY, initialMouseX, initialMouseY;

    const move = (e) => {
      let clientX, clientY;
      if (e.touches) {
        clientX = e.touches[0].clientX;
        clientY = e.touches[0].clientY;
      } else {
        clientX = e.clientX;
        clientY = e.clientY;
      }

      const dx = clientX - initialMouseX;
      const dy = clientY - initialMouseY;

      let newTop = startY + dy;
      let newLeft = startX + dx;

      const parentRect = el.parentElement.getBoundingClientRect();
      const elRect = el.getBoundingClientRect();

      if (newLeft < 0) {
        newLeft = 0;
      } else if (newLeft + elRect.width > parentRect.width) {
        newLeft = parentRect.width - elRect.width;
      }

      if (newTop < 0) {
        newTop = 0;
      } else if (newTop + elRect.height > parentRect.height) {
        newTop = parentRect.height - elRect.height;
      }

      el.style.top = `${newTop}px`;
      el.style.left = `${newLeft}px`;
    };

    const up = () => {
      document.removeEventListener('mousemove', move);
      document.removeEventListener('mouseup', up);
      document.removeEventListener('touchmove', move);
      document.removeEventListener('touchend', up);
    };

    const down = (e) => {
      startX = el.offsetLeft;
      startY = el.offsetTop;
      if (e.touches) {
        initialMouseX = e.touches[0].clientX;
        initialMouseY = e.touches[0].clientY;
      } else {
        initialMouseX = e.clientX;
        initialMouseY = e.clientY;
      }
      document.addEventListener('mousemove', move);
      document.addEventListener('mouseup', up);
      document.addEventListener('touchmove', move);
      document.addEventListener('touchend', up);
      e.preventDefault();
    };

    el.addEventListener('mousedown', down);
    el.addEventListener('touchstart', down);
  }
};

添加节流优化性能

为了防止 mousemove 和 touchmove 事件触发得太频繁,我们可以使用节流(throttle)技术来优化性能。

// src/directives/draggable.js

function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function (...args) {
    const context = this;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function () {
        if (Date.now() - lastRan >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

export default {
  mounted(el) {
    el.style.position = 'absolute';

    let startX, startY, initialMouseX, initialMouseY;

    const move = throttle((e) => {
      let clientX, clientY;
      if (e.touches) {
        clientX = e.touches[0].clientX;
        clientY = e.touches[0].clientY;
      } else {
        clientX = e.clientX;
        clientY = e.clientY;
      }

      const dx = clientX - initialMouseX;
      const dy = clientY - initialMouseY;

      let newTop = startY + dy;
      let newLeft = startX + dx;

      const parentRect = el.parentElement.getBoundingClientRect();
      const elRect = el.getBoundingClientRect();

      if (newLeft < 0) {
        newLeft = 0;
      } else if (newLeft + elRect.width > parentRect.width) {
        newLeft = parentRect.width - elRect.width;
      }

      if (newTop < 0) {
        newTop = 0;
      } else if (newTop + elRect.height > parentRect.height) {
        newTop = parentRect.height - elRect.height;
      }

      el.style.top = `${newTop}px`;
      el.style.left = `${newLeft}px`;
    }, 20);

    const up = () => {
      document.removeEventListener('mousemove', move);
      document.removeEventListener('mouseup', up);
      document.removeEventListener('touchmove', move);
      document.removeEventListener('touchend', up);
    };

    const down = (e) => {
      startX = el.offsetLeft;
      startY = el.offsetTop;
      if (e.touches) {
        initialMouseX = e.touches[0].clientX;
        initialMouseY = e.touches[0].clientY;
      } else {
        initialMouseX = e.clientX;
        initialMouseY = e.clientY;
      }
      document.addEventListener('mousemove', move);
      document.addEventListener('mouseup', up);
      document.addEventListener('touchmove', move);
      document.addEventListener('touchend', up);
      e.preventDefault();
    };

    el.addEventListener('mousedown', down);
    el.addEventListener('touchstart', down);
  }
};

提供配置选项

最后,我们可以通过指令的参数来提供一些配置选项,例如是否限制在父元素内拖拽。

      const dx = clientX - initialMouseX;
      const dy = clientY - initialMouseY;

      let newTop = startY + dy;
      let newLeft = startX + dx;

      if (limitToParent) {
        const parentRect = el.parentElement.getBoundingClientRect();
        const elRect = el.getBoundingClientRect();

        if (newLeft < 0) {
          newLeft = 0;
        } else if (newLeft + elRect.width > parentRect.width) {
          newLeft = parentRect.width - elRect.width;
        }

        if (newTop < 0) {
          newTop = 0;
        } else if (newTop + elRect.height > parentRect.height) {
          newTop = parentRect.height - elRect.height;
        }
      }

      el.style.top = `${newTop}px`;
      el.style.left = `${newLeft}px`;
    }, 20);

    const up = () => {
      document.removeEventListener('mousemove', move);
      document.removeEventListener('mouseup', up);
      document.removeEventListener('touchmove', move);
      document.removeEventListener('touchend', up);
    };

    const down = (e) => {
      startX = el.offsetLeft;
      startY = el.offsetTop;
      if (e.touches) {
        initialMouseX = e.touches[0].clientX;
        initialMouseY = e.touches[0].clientY;
      } else {
        initialMouseX = e.clientX;
        initialMouseY = e.clientY;
      }
      document.addEventListener('mousemove', move);
      document.addEventListener('mouseup', up);
      document.addEventListener('touchmove', move);
      document.addEventListener('touchend', up);
      e.preventDefault();
    };

    el.addEventListener('mousedown', down);
    el.addEventListener('touchstart', down);
  }
};

使用配置选项

现在我们可以通过在使用指令时传递参数来控制是否限制拖拽范围。例如,编辑 src/App.vue:

<template>
  <div>
    <h1>Vue 3 拖拽指令示例</h1>
    <div v-draggable:limit class="draggable-box">拖拽我!</div>
    <div v-draggable class="draggable-box" style="margin-top: 200px;">我可以拖出容器</div>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style>
.draggable-box {
  width: 150px;
  height: 150px;
  background-color: lightblue;
  text-align: center;
  line-height: 150px;
  cursor: move;
  user-select: none;
  margin-bottom: 20px;
}
</style>

在上面的例子中,第一个 div 使用了 v-draggable:limit 指令,这意味着它的拖拽范围将被限制在父元素内。而第二个 div 则没有这个限制,可以自由拖动。

总结

通过本文的详细讲解,我们成功实现并优化了一个功能强大的拖拽指令 v-draggable。该指令不仅支持鼠标操作,还兼容触摸设备,并且通过节流机制有效地提升了性能。此外,我们还实现了限制拖拽范围的功能,使得该指令能够适应更多复杂的应用场景。希望本文能帮助您理解和掌握 Vue 3 中自定义指令的封装与优化技巧。