实现一个鼠标滚轮横向滚动需求

在前端开发中,我们经常会遇到需要实现横向滚动的场景,如图片画廊、时间轴、横向导航等。然而,浏览器默认的滚轮事件只支持垂直方向滚动,如何优雅地实现鼠标滚轮控制横向滚动成为了一个常见的需求。本文将介绍如何使用 Vue 3 的组合式 API 创建一个自定义 Hook 来解决这个问题。

问题分析

在实现横向滚动时,我们面临两个主要挑战:

  1. 如何区分需要横向滚动和垂直滚动的场景
  2. 如何兼容不同的 UI 组件库(如 Element Plus)和原生 DOM 元素

实现思路

我们的解决方案需要具备以下特性:

  • 能够智能判断何时应该进行横向滚动
  • 兼容 Element Plus 的 Scrollbar 组件和原生 DOM 元素
  • 提供平滑的滚动体验
  • 在无法横向滚动时回退到默认垂直滚动行为

代码实现

javascript 复制代码
import { computed, nextTick } from 'vue';

/**
 * 判断是否为数组
 * @param {*} arr - 需要判断的值
 * @returns {boolean} 是否为数组
 */
export const isArray = (arr) => {
  return Array.isArray(arr);
};

/**
 * 鼠标滚轮横向滚动处理 Hook
 * @param {string} [refName] - 可选的 ref 名称,用于 Element Plus Scrollbar 组件
 * @returns {Object} 包含 handleWheel 方法的对象
 */
export function useWheel(refName) {
  // 获取模板引用
  const scrollbarRef = refName ? useTemplateRef(refName) : ref(null);
  
  // 计算目标引用,处理数组情况
  const targetRef = computed(() => {
    return isArray(scrollbarRef.value) ? scrollbarRef.value[0] : scrollbarRef.value;
  });

  /**
   * 处理鼠标滚轮事件
   * @param {Event} event - 滚轮事件对象
   */
  const handleWheel = async (event) => {
    await nextTick();
    
    // 确定滚动容器
    let scrollContainer;
    if (targetRef.value) {
      // Element Plus Scrollbar 组件
      scrollContainer = targetRef.value.$el?.querySelector(".el-scrollbar__wrap");
    } else {
      // 原生 DOM 元素
      scrollContainer = event.currentTarget;
    }
    
    if (!scrollContainer) return;

    // 获取滚动容器信息
    const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
    const maxScrollLeft = scrollWidth - clientWidth;
    const delta = event.deltaY;

    // 判断是否需要横向滚动
    const canScrollRight = delta > 0 && scrollLeft < maxScrollLeft;
    const canScrollLeft = delta < 0 && scrollLeft > 0;

    // 如果可以横向滚动,则执行横向滚动并阻止默认行为
    if (maxScrollLeft > 0 && (canScrollRight || canScrollLeft)) {
      scrollContainer.scrollTo({
        left: scrollLeft + delta * 2, // 乘以2增加滚动速度
        behavior: "smooth" // 平滑滚动效果
      });
      event.preventDefault();
    }
    // 无法横向滚动时允许默认垂直滚动
  };

  return {
    handleWheel,
  };
}

使用示例

在 Element Plus Scrollbar 组件中使用

vue 复制代码
<template>
  <el-scrollbar ref="scrollbar" @wheel="handleWheel">
    <!-- 横向内容 -->
    <div class="horizontal-content">
      <div v-for="item in 20" :key="item" class="card">
        Card {{ item }}
      </div>
    </div>
  </el-scrollbar>
</template>

<script setup>
import { useWheel } from "@/hooks/useWheel";

const { handleWheel } = useWheel("scrollbar");
</script>

<style scoped>
.horizontal-content {
  display: flex;
  width: max-content;
}

.card {
  width: 200px;
  height: 150px;
  margin: 10px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}
</style>

在原生 DOM 元素中使用

vue 复制代码
<template>
  <div class="custom-scroll-container" @wheel="handleWheel">
    <!-- 横向内容 -->
    <div class="horizontal-content">
      <div v-for="item in 20" :key="item" class="card">
        Card {{ item }
      </div>
    </div>
  </div>
</template>

<script setup>
import { useWheel } from "@/hooks/useWheel";

const { handleWheel } = useWheel();
</script>

<style scoped>
.custom-scroll-container {
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

.horizontal-content {
  display: flex;
  width: max-content;
}

.card {
  width: 200px;
  height: 150px;
  margin: 10px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}
</style>

实现原理详解

1. 引用处理

Hook 通过 useTemplateRef 获取对目标元素的引用,并处理了引用可能是数组的情况(Vue 3 中模板引用有时会是数组)。

2. 容器确定

根据是否提供了 ref 名称,Hook 能够智能确定要操作的滚动容器:

  • 对于 Element Plus Scrollbar 组件,需要通过 $el.querySelector(".el-scrollbar__wrap") 找到实际的滚动容器
  • 对于原生 DOM 元素,直接使用事件的目标元素

3. 滚动逻辑

通过比较容器的 scrollWidthclientWidth,判断是否存在横向滚动空间。然后根据滚轮方向和当前滚动位置,决定是否应该进行横向滚动。

4. 平滑体验

使用 scrollTo 方法而非直接修改 scrollLeft,配合 behavior: "smooth" 选项,提供平滑的滚动体验。

相关推荐
子兮曰2 小时前
浏览器与 Node.js 全局变量体系详解:从 window 到 global 的核心差异
前端·javascript·node.js
Olrookie2 小时前
ruoyi-vue(十五)——布局设置,导航栏,侧边栏,顶部栏
前端·vue.js·笔记
召摇2 小时前
API 设计最佳实践 Javascript 篇
前端·javascript·vue.js
光影少年2 小时前
vite打包优化有哪些
前端·vite·掘金·金石计划
码间舞2 小时前
文件太大怎么上传?【分组分片上传大文件】-实战记录
前端·vue.js·程序员
bug_kada2 小时前
前端性能优化之图片预加载
前端·性能优化
北漂大橙子2 小时前
运营妹子复制 200 个 URL 手酸到哭,我用 Puppeteer 写了个工具,1 小时搞定!
前端·puppeteer
小桥风满袖2 小时前
极简三分钟ES6 - ES9中Promise扩展
前端·javascript
Mintopia2 小时前
🧑‍💻 用 Next.js 打造全栈项目的 ESLint + Prettier 配置指南
前端·javascript·next.js