在前端开发中,我们经常会遇到需要实现横向滚动的场景,如图片画廊、时间轴、横向导航等。然而,浏览器默认的滚轮事件只支持垂直方向滚动,如何优雅地实现鼠标滚轮控制横向滚动成为了一个常见的需求。本文将介绍如何使用 Vue 3 的组合式 API 创建一个自定义 Hook 来解决这个问题。
问题分析
在实现横向滚动时,我们面临两个主要挑战:
- 如何区分需要横向滚动和垂直滚动的场景
 - 如何兼容不同的 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. 滚动逻辑
通过比较容器的 scrollWidth 和 clientWidth,判断是否存在横向滚动空间。然后根据滚轮方向和当前滚动位置,决定是否应该进行横向滚动。
4. 平滑体验
使用 scrollTo 方法而非直接修改 scrollLeft,配合 behavior: "smooth" 选项,提供平滑的滚动体验。